diff --git a/react/src/YXUIKit/im-kit-ui/package.json b/react/src/YXUIKit/im-kit-ui/package.json index 41732ae..38ed125 100644 --- a/react/src/YXUIKit/im-kit-ui/package.json +++ b/react/src/YXUIKit/im-kit-ui/package.json @@ -1,6 +1,6 @@ { "name": "@xkit-yx/im-kit-ui", - "version": "10.0.1", + "version": "10.3.0", "description": "云信即时通讯组件", "license": "MIT", "main": "lib/index.js", @@ -54,13 +54,13 @@ }, "dependencies": { "@ant-design/icons": "^5.0.1", - "@xkit-yx/im-store-v2": "^0.1.1", - "@xkit-yx/utils": "^0.6.0", + "@xkit-yx/im-store-v2": "^0.2.0", + "@xkit-yx/utils": "^0.7.1", "antd": "^4.16.3", "mobx": "^6.6.1", "mobx-react": "^7.5.2", - "nim-web-sdk-ng": "10.2.700", + "nim-web-sdk-ng": "10.3.0", "react-string-replace": "^1.1.0" }, - "gitHead": "4e09464e7bb40f8b578b2704e7931ea1e6928417" + "gitHead": "5309f0b247ec3584301aa3ec5f8df0c3cbcd0966" } diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/Container.tsx index 638ec32..6f38ce6 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/Container.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react' import P2pChatContainer from './containers/p2pChatContainer' import TeamChatContainer from './containers/teamChatContainer' -import { useStateContext, useEventTracking, Welcome, Utils } from '../common' +import { useStateContext, useEventTracking, Welcome } from '../common' import { RenderP2pCustomMessageOptions } from './components/ChatP2pMessageList' import { RenderTeamCustomMessageOptions } from './components/ChatTeamMessageList' import { ChatMessageInputProps } from './components/ChatMessageInput' diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/index.tsx new file mode 100644 index 0000000..77feb85 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/index.tsx @@ -0,0 +1,119 @@ +import { Drawer, Input, message } from 'antd' +import { observer } from 'mobx-react' +import React, { FC, useState } from 'react' +import { useStateContext, useTranslation } from '../../../common' +import { LoadingOutlined } from '@ant-design/icons' +import { V2NIMAIModelRoleType } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' +import { getAIErrorMap } from '../../../utils' + +export interface ChatAISearchProps { + prefix?: string +} + +export const ChatAISearch: FC = observer( + ({ prefix = 'chat' }) => { + const _prefix = `${prefix}-ai-search` + + const { store } = useStateContext() + const { t } = useTranslation() + + const aiErrorMap = getAIErrorMap(t) + + const [inputValue, setInputValue] = useState('') + + const title = store.aiUserStore.aiProxying ? ( +
+ + {t('aiSearchingText')} +
+ ) : ( +
{t('aiSearchText')}
+ ) + + const onInputChangeHandler = ( + e: React.ChangeEvent + ) => { + setInputValue(e.target.value) + } + + const onPressEnterHandler = ( + e: React.KeyboardEvent + ) => { + const trimValue = inputValue.trim() + + if (!e.shiftKey) { + e.preventDefault() + if (!trimValue) { + message.warning(t('sendEmptyText')) + return + } + + const aiSearchUser = store.aiUserStore.getAISearchUser() + + if (aiSearchUser) { + store.aiUserStore + .sendAIProxyActive({ + accountId: aiSearchUser.accountId, + content: { msg: trimValue, type: 0 }, + messages: store.aiUserStore.aiReqMsgs.map((item) => ({ + role: 'user' as V2NIMAIModelRoleType, + ...item, + })), + onSendAIProxyErrorHandler: (code: number) => { + const errorText = aiErrorMap[code] || t('aiProxyFailedText') + + message.error(errorText) + }, + }) + .catch(() => { + // + }) + } + + setInputValue('') + } + } + + const onCloseHandler = () => { + store.aiUserStore.resetAIProxy() + } + + return ( + +
+ +
{t('searchTipText')}
+
+ {store.aiUserStore.aiResMsgs + .slice() + .reverse() + .map((item, index) => ( +
+ {item} +
+ ))} +
+
+
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.less b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.less new file mode 100644 index 0000000..34bdb8b --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.less @@ -0,0 +1,52 @@ +@import '../../../style/theme.less'; +@import '~antd/lib/style/themes/variable.less'; + +@prefix-cls: ~'@{chat-prefix}-ai-search'; + +.@{prefix-cls} { + &-content { + display: flex; + flex-direction: column; + color: @yx-primary-text-color; + font-size: @yx-primary-font-size; + height: 100%; + } + + .@{ant-prefix}-drawer-header-title { + flex-flow: row-reverse; + } + + &-title { + display: flex; + align-items: center; + } + + &-textarea { + resize: 'none'; + line-height: '22px'; + padding: 12px; + + &.@{ant-prefix}-input { + min-height: 48px; + } + } + + &-tip { + color: @yx-text-color-2; + font-size: @yx-font-size-12; + text-align: right; + margin-top: 5px; + } + + &-list { + overflow-y: auto; + padding: 0 16px; + } + + &-item { + padding: 16px 0; + &:not(:last-child) { + border-bottom: 1px solid @yx-border-color-3; + } + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.ts new file mode 100644 index 0000000..aef9274 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAISearch/style/index.ts @@ -0,0 +1,5 @@ +import 'antd/lib/input/style' +import 'antd/lib/drawer/style' +import 'antd/lib/message/style' + +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAITranslate/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAITranslate/index.tsx new file mode 100644 index 0000000..18cdc5d --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatAITranslate/index.tsx @@ -0,0 +1,140 @@ +import { observer } from 'mobx-react' +import React, { FC, useEffect, useState } from 'react' +import { useStateContext, useTranslation } from '../../../common' +import { Select, Button, message } from 'antd' +import { + ArrowDownOutlined, + CloseOutlined, + LoadingOutlined, +} from '@ant-design/icons' +import { getAIErrorMap, logger } from '../../../utils' +import { V2NIMError } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/types' + +export interface ChatAITranslateProps { + inputValue: string + setInputValue: (value: string) => void + onClose: () => void + visible: boolean + + prefix?: string +} + +export const ChatAITranslate: FC = observer( + ({ inputValue, setInputValue, onClose, visible, prefix = 'chat' }) => { + const _prefix = `${prefix}-ai-translate` + + const { store } = useStateContext() + const { t } = useTranslation() + + const aiErrorMap = getAIErrorMap(t) + + const options = store.aiUserStore.getAITranslateLangs().map((lang) => ({ + label: lang, + value: lang, + })) + + const resonpse = store.aiUserStore.isAITranslating() + ? store.aiUserStore.aiResMsgs[0] + : '' + + const [selectedLang, setSelectedLang] = useState(options[0].value) + + const resetState = () => { + setSelectedLang(options[0].value) + } + + useEffect(() => { + resetState() + store.aiUserStore.resetAIProxy() + }, [visible, store.aiUserStore]) + + useEffect(() => { + store.aiUserStore.resetAIProxy() + }, [inputValue, store.aiUserStore, selectedLang]) + + const onUseTranslateHandler = () => { + setInputValue(resonpse) + } + + const onTranslateHandler = async () => { + if (!inputValue) { + message.warning(t('aiTranslateEmptyText')) + return + } + + const aiTranslateUser = store.aiUserStore.getAITranslateUser() + + if (aiTranslateUser) { + try { + await store.aiUserStore.sendAIProxyActive({ + accountId: aiTranslateUser.accountId, + requestId: Math.random().toString(36).slice(2), + content: { msg: inputValue, type: 0 }, + promptVariables: JSON.stringify({ Language: selectedLang }), + onSendAIProxyErrorHandler: (code: number) => { + const errorText = aiErrorMap[code] || t('aiProxyFailedText') + + message.error(errorText) + }, + }) + } catch (error) { + logger.error('AI 翻译失败', (error as V2NIMError).toString()) + } + } + } + + const renderBtn = () => { + return store.aiUserStore.aiProxying ? ( +
+ + {t('aiTranslatingText')} +
+ ) : resonpse ? ( + + ) : ( + + ) + } + + return visible ? ( +
+ - } - onOk={handleOk} - onCancel={onCancel} - prefix={commonPrefix} - /> - ) -} + const handleSelectChange = (value: SelectModalItemProps[]) => { + setSelected(value.map((item) => item.key)) + } + + const handleSelectDelete = (value: SelectModalItemProps) => { + setSelected(selected.filter((item) => item !== value.key)) + } + + return ( + + } + onSelectChange={handleSelectChange} + onDelete={handleSelectDelete} + onOk={handleOk} + onCancel={onCancel} + prefix={commonPrefix} + /> + ) + } +) export default ChatForwardModal diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.less b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.less index 703c530..61d0bc1 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.less @@ -1,4 +1,5 @@ @import '../../../style/theme.less'; +@import '~antd/lib/style/themes/variable.less'; @prefix-cls: ~'@{chat-prefix}-forward-modal'; @@ -7,3 +8,60 @@ border-radius: @yx-primary-border-radius; border-color: @yx-border-color-10; } + +.@{prefix-cls}-tabs { + .@{ant-prefix}-tabs-nav-list { + width: 100%; + justify-content: space-between; + } + + .@{ant-prefix}-tabs-tab { + margin: 0; + padding: 14px 28px; + } +} + +.@{prefix-cls}-recent { + display: flex; + flex-direction: column; + + &-title { + font-size: @yx-font-size-12; + color: @yx-text-color-2; + margin-top: 16px; + margin-bottom: 8px; + } + + &-group { + display: flex; + flex-direction: row; + align-items: center; + } + + &-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + &:not(:last-child) { + margin-right: 16px; + } + + .@{ant-prefix}-checkbox-wrapper { + margin: 0; + } + } + + &-label { + font-size: @yx-font-size-12; + color: @yx-primary-text-color; + margin: 5px 0; + display: inline-block; + width: 45px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.ts index c133b08..703632a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatForwardModal/style/index.ts @@ -1,4 +1,5 @@ import 'antd/lib/input/style' -import 'antd/lib/message/style' +import 'antd/lib/tabs/style' +import 'antd/lib/checkbox/style' import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatGroupTransferModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatGroupTransferModal/index.tsx index 39aa115..dedc78c 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatGroupTransferModal/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatGroupTransferModal/index.tsx @@ -4,11 +4,13 @@ import { ComplexAvatarContainer, useStateContext, useTranslation, + SelectModal, } from '../../../common' -import { SelectModal } from '../../../common' import { SelectModalItemProps } from '../../../common/components/SelectModal' import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' import { V2NIMConst } from 'nim-web-sdk-ng' +import { observer } from 'mobx-react' + interface GroupActionModalProps { visible: boolean members: V2NIMTeamMember[] // 成员列表 @@ -18,95 +20,96 @@ interface GroupActionModalProps { teamId: string } -const GroupTransferModal: React.FC = ({ - members, - onOk, - visible, - onCancel, - teamId, - commonPrefix = 'common', -}) => { - const [selectedMemberId, setSelectedMemberId] = useState('') // 选中的成员 ID +const GroupTransferModal: React.FC = observer( + ({ members, onOk, visible, onCancel, teamId, commonPrefix = 'common' }) => { + const [selectedMemberId, setSelectedMemberId] = useState('') // 选中的成员 ID - const { t } = useTranslation() + const { t } = useTranslation() - const { store } = useStateContext() + const { store } = useStateContext() - const handleCancel = () => { - onCancel() - setSelectedMemberId('') - } + const handleCancel = () => { + onCancel() + setSelectedMemberId('') + } - const handleOk = async () => { - try { - await store.teamStore.transferTeamActive({ - account: selectedMemberId, - teamId, - }) - message.success(t('transferTeamSuccessText')) - onOk() - } catch (error: any) { - switch (error?.code) { - // 无权限 - case 109427: - message.error(t('noPermission')) - break - default: - message.error(t('transferTeamFailedText')) - break + const handleOk = async () => { + try { + await store.teamStore.transferTeamActive({ + account: selectedMemberId, + teamId, + }) + message.success(t('transferTeamSuccessText')) + onOk() + } catch (error: any) { + switch (error?.code) { + // 无权限 + case 109427: + message.error(t('noPermission')) + break + default: + message.error(t('transferTeamFailedText')) + break + } } } - } - const handleSelect = (value: SelectModalItemProps[]) => { - setSelectedMemberId(value[0].key) - } + const handleSelect = (value: SelectModalItemProps[]) => { + setSelectedMemberId(value[0].key) + } - const datasource: SelectModalItemProps[] = useMemo(() => { - const _showMembers = members.map((item) => { - return { - ...item, - key: item.accountId, - disabled: - item.memberRole === - V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER, - label: store.uiStore.getAppellation({ - account: item.accountId, - teamId: item.teamId, - }), - } - }) - return _showMembers - }, [members, store.uiStore]) + const datasource: SelectModalItemProps[] = useMemo(() => { + const aiUsers = store.aiUserStore.getAIUserList() + const _showMembers = members + .filter((item) => + aiUsers.every((ai) => ai.accountId !== item.accountId) + ) + .map((item) => { + return { + ...item, + key: item.accountId, + disabled: + item.memberRole === + V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER, + label: store.uiStore.getAppellation({ + account: item.accountId, + teamId: item.teamId, + }), + } + }) + + return _showMembers + }, [members, store.uiStore, store.aiUserStore]) + + const itemAvatarRender = (data: SelectModalItemProps) => { + return ( + + ) + } - const itemAvatarRender = (data: SelectModalItemProps) => { return ( - ) } - - return ( - - ) -} +) export default GroupTransferModal diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/ChatMentionMemberList.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/ChatMentionMemberList.tsx index 5fb9188..55f781a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/ChatMentionMemberList.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/ChatMentionMemberList.tsx @@ -9,6 +9,7 @@ import classNames from 'classnames' import { observer } from 'mobx-react' import { storeConstants } from '@xkit-yx/im-store-v2' import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' export type MentionedMember = { account: string; appellation: string } @@ -16,7 +17,7 @@ export interface ChatMentionMemberList { allowAtAll?: boolean prefix?: string commonPrefix?: string - mentionMembers?: V2NIMTeamMember[] + mentionMembers?: (V2NIMTeamMember | V2NIMAIUser)[] onSelect?: (member: MentionedMember) => void } @@ -48,9 +49,11 @@ export const ChatAtMemberList: React.FC = observer( const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowUp') { const index = activeIndex - 1 + setActiveIndex(index < -1 ? maxIndex : index) } else if (e.key === 'ArrowDown') { const index = activeIndex + 1 + setActiveIndex(index > maxIndex ? -1 : index) } else if (e.key === 'Enter') { if (activeIndex === -1) { @@ -60,17 +63,19 @@ export const ChatAtMemberList: React.FC = observer( }) } else { const member = mentionMembers[activeIndex] + onSelect?.({ account: member.accountId, appellation: store.uiStore.getAppellation({ account: member.accountId, - teamId: member.teamId, + teamId: (member as V2NIMTeamMember).teamId, ignoreAlias: true, }), }) } } } + document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('keydown', handleKeyDown) @@ -110,7 +115,7 @@ export const ChatAtMemberList: React.FC = observer( account: member.accountId, appellation: store.uiStore.getAppellation({ account: member.accountId, - teamId: member.teamId, + teamId: (member as V2NIMTeamMember).teamId, ignoreAlias: true, }), }) @@ -126,7 +131,7 @@ export const ChatAtMemberList: React.FC = observer( {store.uiStore.getAppellation({ account: member.accountId, - teamId: member.teamId, + teamId: (member as V2NIMTeamMember).teamId, })}
diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/index.tsx index c0bd0fc..a885657 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/index.tsx @@ -8,16 +8,7 @@ import React, { useCallback, useEffect, } from 'react' -import { - Input, - Upload, - Popover, - message, - Button, - Dropdown, - Menu, - Spin, -} from 'antd' +import { Input, Upload, Popover, message, Button, Dropdown, Menu } from 'antd' import { CommonIcon, getMsgContentTipByType, @@ -28,15 +19,20 @@ import { import { Action } from '../../Container' import { MAX_UPLOAD_FILE_SIZE } from '../../../constant' import { storeConstants } from '@xkit-yx/im-store-v2' -import { LoadingOutlined, CloseOutlined } from '@ant-design/icons' +import { CloseOutlined } from '@ant-design/icons' import { observer } from 'mobx-react' import { TextAreaRef } from 'antd/lib/input/TextArea' import ChatAtMemberList, { MentionedMember } from './ChatMentionMemberList' import { mergeActions } from '../../../utils' -import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types' +import { + V2NIMMessageForUI, + YxAitMsg, + YxServerExt, +} from '@xkit-yx/im-store-v2/dist/types/types' import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMConversationService' import { V2NIMConst } from 'nim-web-sdk-ng' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' const { TextArea } = Input @@ -45,15 +41,17 @@ export interface ChatMessageInputProps { commonPrefix?: string placeholder?: string replyMsg?: V2NIMMessageForUI - mentionMembers?: V2NIMTeamMember[] + mentionMembers?: (V2NIMTeamMember | V2NIMAIUser)[] conversationType: V2NIMConversationType receiverId: string actions?: Action[] mute?: boolean allowAtAll?: boolean inputValue?: string + translateOpen: boolean setInputValue: (value: string) => void - onSendText: (value: string, ext?: Record) => void + onTranslate: (open: boolean) => void + onSendText: (value: string, ext?: YxServerExt) => void onSendFile: (file: File) => void onSendImg: (file: File) => void onSendVideo: (file: File) => void @@ -70,20 +68,21 @@ const ChatMessageInput = observer( forwardRef< ChatMessageInputRef, React.PropsWithChildren - >((props, ref) => { + >(function ChatMessageInputContent(props, ref) { const { prefix = 'chat', commonPrefix = 'common', placeholder = '', mentionMembers, actions, - conversationType, receiverId, mute = false, allowAtAll = true, inputValue = '', + translateOpen, replyMsg, setInputValue, + onTranslate, onSendText, onSendFile, onSendImg, @@ -106,9 +105,9 @@ const ChatMessageInput = observer( const { EMOJI_ICON_MAP_CONFIG } = Utils.handleEmojiTranslate(t) - const LoadingIcon = ( - - ) + const onTranslateHandler = () => { + onTranslate(!translateOpen) + } const defaultActions: Action[] = [ { @@ -220,6 +219,25 @@ const ChatMessageInput = observer( ) }, }, + { + action: 'aiTranslate', + visible: + localOptions.aiVisible && !!store.aiUserStore.getAITranslateUser(), + render: () => { + return ( + + ) + }, + }, { action: 'sendMsg', visible: true, @@ -256,10 +274,11 @@ const ChatMessageInput = observer( return store.uiStore .getAppellation({ account: member.accountId, - teamId: member.teamId, + teamId: (member as V2NIMTeamMember).teamId, }) ?.includes(atMemberSearchText.replace('@', '')) }) + return res } else { return mentionMembers @@ -284,7 +303,8 @@ const ChatMessageInput = observer( }, [filterAtMembers, atMemberSearchText]) const onAtMembersExtHandler = () => { - let ext + let ext: YxServerExt | void = void 0 + if (selectedAtMembers.length) { selectedAtMembers .filter((member) => { @@ -294,16 +314,19 @@ const ChatMessageInput = observer( ) { return false } + return true }) .forEach((member) => { const substr = `@${member.appellation} ` const positions: number[] = [] let pos = inputValue.indexOf(substr) + while (pos !== -1) { positions.push(pos) pos = inputValue.indexOf(substr, pos + 1) } + if (positions.length) { if (!ext) { ext = { @@ -315,14 +338,16 @@ const ChatMessageInput = observer( }, } } else { - ext.yxAitMsg[member.account] = { + ;(ext.yxAitMsg as YxAitMsg)[member.account] = { text: substr, segments: [], } } + positions.forEach((position) => { const start = position - ext.yxAitMsg[member.account].segments.push({ + + ;(ext?.yxAitMsg as YxAitMsg)[member.account].segments.push({ start, end: start + substr.length - 1, broken: false, @@ -331,7 +356,8 @@ const ChatMessageInput = observer( } }) } - return ext + + return ext as unknown as YxServerExt } const onTextAreaSelectionRangeHandler = () => { @@ -339,13 +365,16 @@ const ChatMessageInput = observer( cursorIndex: number ): { selectionStart: number; selectionEnd: number } | undefined { let selectionStart, selectionEnd + selectedAtMembers.some((member) => { const alias = `@${member.appellation} ` const regex = new RegExp(alias, 'g') let match + while ((match = regex.exec(inputValue))) { const start = match.index const end = start + alias.length + if (cursorIndex > start && cursorIndex < end) { selectionStart = start selectionEnd = end @@ -358,8 +387,10 @@ const ChatMessageInput = observer( selectionEnd: selectionEnd ?? cursorIndex, } } + setTimeout(() => { const input = textAreaRef.current?.resizableTextArea?.textArea + //当needMention为false时 避免不必要的计算 if (input && localOptions.needMention) { const selectionStart = @@ -368,6 +399,7 @@ const ChatMessageInput = observer( const selectionEnd = getCursorPosition(input.selectionEnd)?.selectionEnd ?? input.selectionEnd + input.setSelectionRange(selectionStart, selectionEnd) } }, 0) @@ -377,17 +409,21 @@ const ChatMessageInput = observer( const newValue = e.target.value as string const input = textAreaRef.current?.resizableTextArea?.textArea let atMemberSearchText = '' + if (input) { const cursorIndex = input.selectionStart const subStr = newValue.slice(0, cursorIndex) const atIndex = subStr.lastIndexOf('@') + if (atIndex !== -1) { atMemberSearchText = newValue.slice(atIndex, cursorIndex) } } + if (localOptions.needMention) { setAtMemberSearchText(atMemberSearchText) } + setInputValue(newValue) } @@ -400,6 +436,7 @@ const ChatMessageInput = observer( member, ]) const input = textAreaRef.current?.resizableTextArea?.textArea + if (input) { input.focus() input.setRangeText( @@ -412,6 +449,7 @@ const ChatMessageInput = observer( ) setInputValue(input.value) } + setAtMemberSearchText('') }, [atMemberSearchText, selectedAtMembers, setInputValue, atVisible] @@ -424,11 +462,14 @@ const ChatMessageInput = observer( setAtMemberSearchText('') return } + const trimValue = inputValue.trim() + if (!trimValue) { message.warning(t('sendEmptyText')) return } + onSendText(inputValue, onAtMembersExtHandler()) setInputValue('') setSelectedAtMembers([]) @@ -441,13 +482,16 @@ const ChatMessageInput = observer( e.preventDefault() return } + const trimValue = inputValue.trim() + if (!e.shiftKey) { e.preventDefault() if (!trimValue) { message.warning(t('sendEmptyText')) return } + onSendText(inputValue, onAtMembersExtHandler()) setInputValue('') setSelectedAtMembers([]) @@ -462,16 +506,20 @@ const ChatMessageInput = observer( e.nativeEvent.preventDefault() } else if (e.key === 'Backspace') { const input = textAreaRef.current?.resizableTextArea?.textArea + if (input) { const cursorIndex = input?.selectionStart let atIndex + selectedAtMembers.some((member) => { const alias = `@${member.appellation} ` const regex = new RegExp(alias, 'g') let match + while ((match = regex.exec(inputValue))) { const start = match.index const end = start + alias.length + if (cursorIndex === end) { atIndex = start return true @@ -556,17 +604,19 @@ const ChatMessageInput = observer( const onEmojiClickHandler = (tag: string) => { const input = textAreaRef.current?.resizableTextArea?.textArea + if (input) { input.focus() input.setRangeText(tag, input.selectionStart, input.selectionEnd, 'end') setInputValue(input.value) } + setEmojiVisible(false) } const emojiContent = ( <> - {Object.keys(EMOJI_ICON_MAP_CONFIG).map((tag: string, index) => ( + {Object.keys(EMOJI_ICON_MAP_CONFIG).map((tag: string) => ( { onEmojiClickHandler(tag) @@ -595,8 +645,11 @@ const ChatMessageInput = observer( : undefined, ignoreAlias: true, }) - let content = `${t('replyText')} ${nick}:` - content += replyMsg ? getMsgContentTipByType(replyMsg, t) : '' + const content: React.ReactNode[] = [ + <>{`${t('replyText')} ${nick}:`}, + ] + + content.push(replyMsg ? getMsgContentTipByType(replyMsg, t) : '') return
{content}
} } diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/style/index.less b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/style/index.less index 14adb09..df99d3a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageInput/style/index.less @@ -112,11 +112,21 @@ .@{prefix-cls}-icon-emoji, .@{prefix-cls}-icon-image, + .@{prefix-cls}-icon-translate, .@{prefix-cls}-icon-file { cursor: pointer; color: @yx-icon-color-1; } + .@{prefix-cls}-icon-translate-active, + .@{prefix-cls}-icon-translate { + font-size: @yx-font-size-20; + } + + .@{prefix-cls}-icon-translate-active { + color: @yx-primary-button-color; + } + .@{prefix-cls}-icon-upload { line-height: 0px; diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/index.tsx index 46cdd90..25ced09 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/index.tsx @@ -1,8 +1,12 @@ import React, { Fragment, useRef } from 'react' import { Dropdown, Menu, Tooltip } from 'antd' -import { LoadingOutlined, ExclamationCircleFilled } from '@ant-design/icons' +import { + LoadingOutlined, + ExclamationCircleFilled, + RollbackOutlined, + DeleteOutlined, +} from '@ant-design/icons' import classNames from 'classnames' -import moment from 'moment' import { ParseSession, ComplexAvatarContainer, @@ -11,14 +15,24 @@ import { CommonIcon, useStateContext, } from '../../../common' -import { RollbackOutlined, DeleteOutlined } from '@ant-design/icons' import { observer } from 'mobx-react' import { MsgOperMenuItem } from '../../Container' -import { mergeActions } from '../../../utils' -import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types' +import { formatDate, getAIErrorMap, mergeActions } from '../../../utils' +import { + V2NIMMessageForUI, + YxTopMessage, +} from '@xkit-yx/im-store-v2/dist/types/types' import { V2NIMConst } from 'nim-web-sdk-ng' -export type MenuItemKey = 'recall' | 'delete' | 'reply' | 'forward' | string +export type MenuItemKey = + | 'recall' + | 'delete' + | 'reply' + | 'collection' + | 'forward' + | 'top' + | 'unTop' + | string export type AvatarMenuItem = 'mention' export interface MenuItem { @@ -34,6 +48,7 @@ export interface MenuItem { export interface MessageItemProps { msg: V2NIMMessageForUI + topMessage?: YxTopMessage replyMsg?: V2NIMMessageForUI normalStatusRenderer?: React.ReactNode msgOperMenu?: MsgOperMenuItem[] @@ -59,6 +74,7 @@ export const ChatMessageItem: React.FC = observer( ({ msg, replyMsg, + topMessage, normalStatusRenderer, msgOperMenu, onMessageAction, @@ -78,12 +94,10 @@ export const ChatMessageItem: React.FC = observer( const _prefix = `${prefix}-message-list-item` const { - text, senderId, receiverId, messageClientId, sendingState, - uploadProgress, createTime, messageType, @@ -91,13 +105,15 @@ export const ChatMessageItem: React.FC = observer( isSelf, recallType = '', canRecall = false, - canEdit = false, errorCode, + messageStatus, } = msg const messageActionDropdownContainerRef = useRef(null) const messageAvatarActionDropdownContainerRef = useRef(null) + const aiErrorMap = getAIErrorMap(t) + const nick = store.uiStore.getAppellation({ account: senderId, teamId: @@ -128,6 +144,7 @@ export const ChatMessageItem: React.FC = observer( ) { return } + if ( sendingState === V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED @@ -138,6 +155,7 @@ export const ChatMessageItem: React.FC = observer( : errorCode === 104404 ? t('sendNotFriendFailedText') : t('sendMsgFailedText') + return ( = observer( ) } - return normalStatusRenderer || null - } - const renderMsgDate = () => { - const date = moment(createTime) - const isCurrentDay = date.isSame(moment(), 'day') - const isCurrentYear = date.isSame(moment(), 'year') - return isCurrentDay - ? date.format('HH:mm:ss') - : isCurrentYear - ? date.format('MM-DD HH:mm:ss') - : date.format('YYYY-MM-DD HH:mm:ss') + return normalStatusRenderer || null } const renderMenuItems = () => { @@ -202,6 +210,61 @@ export const ChatMessageItem: React.FC = observer( key: 'forward', icon: , }, + { + show: + [ + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_SENDING, + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_FAILED, + ].includes(sendingState) || + messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL + ? 0 + : 1, + label: t('collection'), + key: 'collection', + icon: , + }, + { + show: + conversationType === + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P || + [ + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_SENDING, + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_FAILED, + ].includes(sendingState) || + messageType === + V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL || + (topMessage?.idClient === messageClientId && + topMessage.operation === 0) + ? 0 + : 1, + label: t('topText'), + key: 'top', + icon: , + }, + { + show: + conversationType === + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P || + [ + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_SENDING, + V2NIMConst.V2NIMMessageSendingState + .V2NIM_MESSAGE_SENDING_STATE_FAILED, + ].includes(sendingState) || + messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL + ? 0 + : topMessage?.idClient === messageClientId && + topMessage.operation === 0 + ? 1 + : 0, + label: t('unTopText'), + key: 'unTop', + icon: , + }, { show: canRecall ? 1 : 0, label: t('recallText'), @@ -212,6 +275,7 @@ export const ChatMessageItem: React.FC = observer( const menuItems = msgOperMenu ? mergeActions(defaultMenuItems, msgOperMenu, 'key') : defaultMenuItems + return menuItems.filter((item) => item.show) } @@ -225,30 +289,18 @@ export const ChatMessageItem: React.FC = observer( ] } - const renderSpecialMsg = () => { - return ( -
- {recallType === 'reCallMsg' ? ( - <> - {`${t('you')}${t('recallMessageText')}`} - {canEdit ? ( - onReeditClick(msg)} - > - {t('reeditText')} - - ) : null} - - ) : ( - `${isSelf ? t('you') : nick} ${t('recallMessageText')}` - )} -
- ) - } - - return recallType === 'reCallMsg' || recallType === 'beReCallMsg' ? ( - renderSpecialMsg() + return messageType === + V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIPS && + Object.keys(aiErrorMap) + .map((item) => Number(item)) + .includes(messageStatus.errorCode) ? ( + + ) : recallType === 'reCallMsg' || recallType === 'beReCallMsg' ? ( + ) : messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION ? ( @@ -327,6 +379,7 @@ export const ChatMessageItem: React.FC = observer( replyMsg={replyMsg} msg={msg} prefix={commonPrefix} + showThreadReply={true} /> )} @@ -337,7 +390,7 @@ export const ChatMessageItem: React.FC = observer( [`${_prefix}-date-self`]: isSelf, })} > - {renderMsgDate()} + {formatDate(createTime)} diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/style/index.less b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/style/index.less index 2e8db5f..e02bf36 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatMessageItem/style/index.less @@ -105,16 +105,3 @@ margin-right: 0; } } - -.@{prefix-cls}-recall { - padding: 10px 0; - line-height: 16px; - color: @yx-text-color-2; - text-align: center; - - .@{prefix-cls}-reedit { - cursor: pointer; - padding-left: 6px; - color: @yx-text-color-5; - } -} diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pMessageList/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pMessageList/index.tsx index bf89291..dc79660 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pMessageList/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pMessageList/index.tsx @@ -31,7 +31,7 @@ export interface ChatP2pMessageListProps const ChatP2pMessageList = observer( forwardRef( - ( + function ChatP2pMessageListContent( { prefix = 'chat', commonPrefix = 'common', @@ -55,13 +55,15 @@ const ChatP2pMessageList = observer( renderMessageInnerContent, }, ref - ) => { + ) { const _prefix = `${prefix}-message-list` const { t } = useTranslation() const { store, localOptions } = useStateContext() + const { relation } = store.uiStore.getRelation(receiverId) + const renderMsgs = storeUtils.getFilterMsgs(msgs) return ( @@ -104,6 +106,7 @@ const ChatP2pMessageList = observer( renderMessageOuterContent={renderMessageOuterContent} /> ) + return (
{msgItem} @@ -120,7 +123,7 @@ const ChatP2pMessageList = observer(
) : null} - {store.uiStore.getRelation(receiverId).relation === 'stranger' ? ( + {relation === 'stranger' ? ( void @@ -14,50 +19,86 @@ export interface ChatP2pSettingProps { commonPrefix?: string } -const ChatP2pSetting: React.FC = ({ - onCreateGroupClick, - account, - alias = '', - nick = '', - - prefix = 'chat', - commonPrefix = 'common', -}) => { - const _prefix = `${prefix}-person-setting` - const { t } = useTranslation() - - return ( -
{ - e.stopPropagation() - }} - > -
- - - {alias || nick || account} - -
+const ChatP2pSetting: React.FC = observer( + ({ + onCreateGroupClick, + account, + alias = '', + nick = '', + + prefix = 'chat', + commonPrefix = 'common', + }) => { + const _prefix = `${prefix}-person-setting` + + const { store } = useStateContext() + + const { t } = useTranslation() + + const { relation } = store.uiStore.getRelation(account) + + const isAIPin = + relation === 'ai' ? store.aiUserStore.isAIPinUser(account) : false -
-
+ + {relation === 'ai' && + store.aiUserStore.getAIUserServerExt(account).pinDefault === 1 ? ( +
+ {t('pinAIText')} + +
+ ) : null}
- - ) -} + ) + } +) export default ChatP2pSetting diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pSetting/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pSetting/style/index.ts index a49de71..2baa8d5 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pSetting/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatP2pSetting/style/index.ts @@ -1,2 +1,3 @@ import 'antd/lib/button/style' +import 'antd/lib/switch/style' import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatSettingDrawer/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatSettingDrawer/index.tsx index 5343db7..3ee34e6 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatSettingDrawer/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatSettingDrawer/index.tsx @@ -14,7 +14,6 @@ export interface ChatSettingDrawerProps { const ChatSettingDrawer: React.FC = ({ visible, onClose, - drawerContainer, title, children, prefix = 'chat', diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMemberModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMemberModal/index.tsx index e031974..eee39ea 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMemberModal/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMemberModal/index.tsx @@ -34,13 +34,17 @@ const ChatTeamMemberModal: React.FC = observer( .getTeamMember(teamId) .filter((item) => item.accountId !== store.userStore.myUserInfo.accountId) - const datasource = teamMembers.map((item) => ({ - key: item.accountId, - label: store.uiStore.getAppellation({ - account: item.accountId, - teamId: item.teamId, - }), - })) + const aiUsers = store.aiUserStore.getAIUserList() + + const datasource = teamMembers + .filter((item) => aiUsers.every((ai) => ai.accountId !== item.accountId)) + .map((item) => ({ + key: item.accountId, + label: store.uiStore.getAppellation({ + account: item.accountId, + teamId: item.teamId, + }), + })) const teamManagerAccounts = teamMembers .filter( @@ -69,6 +73,7 @@ const ChatTeamMemberModal: React.FC = observer( const remove = teamManagerAccounts.filter((i) => data.every((j) => j.key !== i) ) + add.length && (await store.teamStore.updateTeamMemberRoleActive({ teamId, diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMessageList/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMessageList/index.tsx index bd75074..3855326 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMessageList/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamMessageList/index.tsx @@ -33,108 +33,110 @@ export interface ChatTeamMessageListProps const ChatTeamMessageList = forwardRef< HTMLDivElement, ChatTeamMessageListProps ->( - ( - { - prefix = 'chat', - commonPrefix = 'common', - msgs, - msgOperMenu, - replyMsgsMap, - members, - receiveMsgBtnVisible = false, - strangerNotiVisible = false, - strangerNotiText = '', - onReceiveMsgBtnClick, - loadingMore, - noMore, - onResend, - onMessageAction, - onMessageAvatarAction, - onReeditClick, - onScroll, - renderTeamCustomMessage, - renderMessageAvatar, - renderMessageName, - renderMessageInnerContent, - renderMessageOuterContent, - }, - ref - ) => { - const _prefix = `${prefix}-message-list` +>(function ChatTeamMessageListContent( + { + prefix = 'chat', + commonPrefix = 'common', + msgs, + topMessage, + msgOperMenu, + replyMsgsMap, + members, + receiveMsgBtnVisible = false, + strangerNotiVisible = false, + strangerNotiText = '', + onReceiveMsgBtnClick, + loadingMore, + noMore, + onResend, + onMessageAction, + onMessageAvatarAction, + onReeditClick, + onScroll, + renderTeamCustomMessage, + renderMessageAvatar, + renderMessageName, + renderMessageInnerContent, + renderMessageOuterContent, + }, + ref +) { + const _prefix = `${prefix}-message-list` - const { t } = useTranslation() + const { t } = useTranslation() - const { localOptions } = useStateContext() + const { localOptions } = useStateContext() - const renderMsgs = storeUtils.getFilterMsgs(msgs) + const renderMsgs = storeUtils.getFilterMsgs(msgs) - return ( -
-
- {noMore ? t('noMoreText') : loadingMore ? : null} -
-
- {renderMsgs.map((msg) => { - const msgItem = renderTeamCustomMessage?.({ - msg, - replyMsg: replyMsgsMap[msg.messageClientId], - members, - onResend, - onReeditClick, - onMessageAction, - }) ?? ( - - ) : null - } - onResend={onResend} - onMessageAction={onMessageAction} - onMessageAvatarAction={onMessageAvatarAction} - onReeditClick={onReeditClick} - renderMessageAvatar={renderMessageAvatar} - renderMessageName={renderMessageName} - renderMessageInnerContent={renderMessageInnerContent} - renderMessageOuterContent={renderMessageOuterContent} - /> - ) - return ( -
- {msgItem} -
- ) - })} -
- {receiveMsgBtnVisible ? ( -
- {t('receiveText')} - -
- ) : null} - {strangerNotiVisible ? ( - - ) : null} + return ( +
+
+ {noMore ? t('noMoreText') : loadingMore ? : null}
- ) - } -) +
+ {renderMsgs.map((msg) => { + const msgItem = renderTeamCustomMessage?.({ + msg, + replyMsg: replyMsgsMap[msg.messageClientId], + topMessage, + members, + onResend, + onReeditClick, + onMessageAction, + }) ?? ( + + ) : null + } + onResend={onResend} + onMessageAction={onMessageAction} + onMessageAvatarAction={onMessageAvatarAction} + onReeditClick={onReeditClick} + renderMessageAvatar={renderMessageAvatar} + renderMessageName={renderMessageName} + renderMessageInnerContent={renderMessageInnerContent} + renderMessageOuterContent={renderMessageOuterContent} + /> + ) + + return ( +
+ {msgItem} +
+ ) + })} +
+ {receiveMsgBtnVisible ? ( +
+ {t('receiveText')} + +
+ ) : null} + {strangerNotiVisible ? ( + + ) : null} +
+ ) +}) export default ChatTeamMessageList diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupDetail.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupDetail.tsx index 2091c71..4f3ee20 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupDetail.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupDetail.tsx @@ -47,6 +47,7 @@ const GroupDetail: FC = ({ const onUpdateTeamInfoSubmitHandler = () => { const obj: V2NIMUpdatedTeamInfo = { avatar, name, intro } + Object.keys(obj).forEach((key) => { if (obj[key] === team[key]) { delete obj[key] diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupList.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupList.tsx index 9c6891b..270b9a6 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupList.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupList.tsx @@ -1,9 +1,10 @@ -import React, { FC, useMemo, useState } from 'react' +import React, { FC, useEffect, useMemo, useState } from 'react' import { GroupItem, GroupItemProps } from './GroupItem' import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' import { Input } from 'antd' import { SearchOutlined } from '@ant-design/icons' import { useStateContext, useTranslation } from '../../../common' +import { observer } from 'mobx-react' export interface GroupListProps { myMemberInfo: V2NIMTeamMember @@ -17,66 +18,83 @@ export interface GroupListProps { commonPrefix?: string } -const GroupList: FC = ({ - myMemberInfo, - members, - onRemoveTeamMemberClick, - afterSendMsgClick, - renderTeamMemberItem, - prefix = 'chat', - commonPrefix = 'common', -}) => { - const _prefix = `${prefix}-group-list` - const { t } = useTranslation() - const { store } = useStateContext() - const [groupSearchText, setGroupSearchText] = useState('') - const handleSearch = (searchText: string) => { - setGroupSearchText(searchText) - } - - const showMembers = useMemo(() => { - let _sortedMembers = members - if (groupSearchText) { - _sortedMembers = members.filter((item) => - store.uiStore - .getAppellation({ account: item.accountId, teamId: item.teamId }) - .includes(groupSearchText) - ) +const GroupList: FC = observer( + ({ + myMemberInfo, + members, + onRemoveTeamMemberClick, + afterSendMsgClick, + renderTeamMemberItem, + prefix = 'chat', + commonPrefix = 'common', + }) => { + const _prefix = `${prefix}-group-list` + const { t } = useTranslation() + const { store } = useStateContext() + const [groupSearchText, setGroupSearchText] = useState('') + const handleSearch = (searchText: string) => { + setGroupSearchText(searchText) } - return _sortedMembers - }, [members, groupSearchText, store.uiStore]) - return ( -
- } - allowClear - className={`${_prefix}-input`} - value={groupSearchText} - placeholder={t('searchTeamMemberPlaceholder')} - onChange={(e) => handleSearch(e.target.value)} - /> - {showMembers.length ? ( - showMembers.map((item) => { - const itemProps = { - member: item, - onRemoveTeamMemberClick, - afterSendMsgClick, - myMemberInfo, - prefix, - commonPrefix, - } - return ( - renderTeamMemberItem?.(itemProps) ?? ( - - ) - ) + useEffect(() => { + members + .filter( + (item) => + !store.userStore.users.has(item.accountId) && + !store.aiUserStore.aiUsers.has(item.accountId) + ) + .forEach((item) => { + store.userStore.getUserActive(item.accountId) }) - ) : ( -
{t('searchNoResText')}
- )} -
- ) -} + }, [members, store.userStore, store.aiUserStore]) + + const showMembers = useMemo(() => { + let _sortedMembers = members + + if (groupSearchText) { + _sortedMembers = members.filter((item) => + store.uiStore + .getAppellation({ account: item.accountId, teamId: item.teamId }) + .includes(groupSearchText) + ) + } + + return _sortedMembers + }, [members, groupSearchText, store.uiStore]) + + return ( +
+ } + allowClear + className={`${_prefix}-input`} + value={groupSearchText} + placeholder={t('searchTeamMemberPlaceholder')} + onChange={(e) => handleSearch(e.target.value)} + /> + {showMembers.length ? ( + showMembers.map((item) => { + const itemProps = { + member: item, + onRemoveTeamMemberClick, + afterSendMsgClick, + myMemberInfo, + prefix, + commonPrefix, + } + + return ( + renderTeamMemberItem?.(itemProps) ?? ( + + ) + ) + }) + ) : ( +
{t('searchNoResText')}
+ )} +
+ ) + } +) export default GroupList diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupPower.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupPower.tsx index f119396..38bd1f2 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupPower.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/GroupPower.tsx @@ -11,8 +11,8 @@ import { V2NIMUpdatedTeamInfo, } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' import ChatTeamMemberModal from '../ChatTeamMemberModal' -import { ALLOW_AT, TAllowAt } from '../../../constant' import { V2NIMConst } from 'nim-web-sdk-ng' +import { YxServerExt } from '@xkit-yx/im-store-v2/dist/types/types' export interface GroupPowerProps { onUpdateTeamInfo: (team: V2NIMUpdatedTeamInfo) => void @@ -58,13 +58,15 @@ const GroupPower: React.FC = ({ ] }, [localOptions.teamManagerVisible, t]) - const ext: TAllowAt = useMemo(() => { + const ext: YxServerExt = useMemo(() => { let res = {} + try { res = JSON.parse(team.serverExtension || '{}') } catch (error) { // } + return res }, [team.serverExtension]) @@ -158,10 +160,36 @@ const GroupPower: React.FC = ({ +
+
+ + diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/index.tsx index c4ef33d..9d8faf5 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTeamSetting/index.tsx @@ -1,7 +1,10 @@ import React, { FC, useState, useEffect, useMemo } from 'react' import { Modal, Button, Input } from 'antd' -import { ExclamationCircleOutlined } from '@ant-design/icons' -import { RightOutlined, PlusOutlined } from '@ant-design/icons' +import { + ExclamationCircleOutlined, + RightOutlined, + PlusOutlined, +} from '@ant-design/icons' import { ComplexAvatarContainer, CrudeAvatar, @@ -107,7 +110,7 @@ const ChatTeamSetting: FC = ({ setNickInTeam(e.target.value.trim()) } - const handleUpdateMyMemberInfo = (e: React.FocusEvent) => { + const handleUpdateMyMemberInfo = () => { onUpdateMyMemberInfo({ teamNick: nickInTeam, }) @@ -150,6 +153,7 @@ const ChatTeamSetting: FC = ({ ) { return true } + return ( team.updateInfoMode === V2NIMConst.V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_ALL diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/index.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/index.tsx new file mode 100644 index 0000000..49f528b --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/index.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from 'react' +import { + V2NIMMessage, + V2NIMMessageDeletedNotification, + V2NIMMessageRevokeNotification, +} from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMMessageService' +import { + CommonIcon, + ParseSession, + useStateContext, + useTranslation, +} from '../../../common' +import { V2NIMConst } from 'nim-web-sdk-ng' +import { logger } from '../../../utils' +import { V2NIMError } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/types' +import { Button } from 'antd' +import { observer } from 'mobx-react' +import { YxTopMessage } from '@xkit-yx/im-store-v2/dist/types/types' + +export interface ChatTopMessageProps { + topMessage: YxTopMessage + allowTop: boolean + onClose: (msg: V2NIMMessage, isTop: boolean) => void + + prefix?: string + commonPrefix?: string +} + +const ChatTopMessage: React.FC = observer( + ({ + topMessage, + allowTop, + onClose, + + prefix = 'chat', + commonPrefix = 'common', + }) => { + const { store, nim } = useStateContext() + const { t } = useTranslation() + const [msg, setMsg] = useState() + const [isShow, setIsShow] = useState(true) + + const _prefix = `${prefix}-top-msg` + + useEffect(() => { + if (topMessage.idClient) { + setIsShow(topMessage.operation === 0) + } + }, [topMessage.idClient, topMessage.operation]) + + useEffect(() => { + const handleMessageDelete = (data: V2NIMMessageDeletedNotification[]) => { + // 如果置顶的消息被删除了,需要不展示置顶消息 + if ( + data.some( + (item) => item.messageRefer.messageClientId === msg?.messageClientId + ) + ) { + setIsShow(false) + } + } + + const handleMessageRevoke = (data: V2NIMMessageRevokeNotification[]) => { + // 如果置顶的消息被撤回了,且无权限更改群聊扩展字段时,需要本地不展示置顶消息 + if ( + data.some( + (item) => item.messageRefer.messageClientId === msg?.messageClientId + ) + ) { + setIsShow(false) + } + } + + nim.V2NIMMessageService.on( + 'onMessageDeletedNotifications', + handleMessageDelete + ) + nim.V2NIMMessageService.on( + 'onMessageRevokeNotifications', + handleMessageRevoke + ) + + return () => { + nim.V2NIMMessageService.off( + 'onMessageDeletedNotifications', + handleMessageDelete + ) + nim.V2NIMMessageService.off( + 'onMessageRevokeNotifications', + handleMessageRevoke + ) + } + }, [nim.V2NIMMessageService, msg?.messageClientId]) + + useEffect(() => { + const params = { + senderId: topMessage.from, + receiverId: topMessage.receiverId, + messageClientId: topMessage.idClient, + messageServerId: topMessage.idServer, + createTime: topMessage.time, + conversationType: topMessage.scene, + conversationId: topMessage.to, + } + + logger.log('获取置顶消息', params) + + nim.V2NIMMessageService.getMessageListByRefers([params]) + .then((res) => { + const _msg = res[0] + + if (_msg) { + // @ts-ignore + delete _msg.uploadProgress + + setMsg(store.msgStore.handleReceiveAIMsg(_msg)) + } + }) + .catch((err) => { + logger.error( + '获取置顶消息失败:', + params, + (err as V2NIMError).toString() + ) + setMsg(undefined) + }) + }, [ + nim.V2NIMConversationIdUtil, + nim.V2NIMMessageService, + topMessage.from, + topMessage.receiverId, + topMessage.to, + topMessage.idClient, + topMessage.idServer, + topMessage.scene, + topMessage.time, + store.msgStore, + ]) + + const handleClose = () => { + if (msg) { + onClose(msg, false) + } + } + + return msg && isShow ? ( +
+
+ + + {store.uiStore.getAppellation({ + account: msg.senderId, + teamId: msg.receiverId, + })} + : + + {msg.messageType === + V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION ? ( + + [{t('geoMsgShortText')}]  + + ) : null} + +
+ {allowTop && ( + + )} +
+ ) : null + } +) + +export default ChatTopMessage diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.less b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.less new file mode 100644 index 0000000..6fcd837 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.less @@ -0,0 +1,86 @@ +@import '~antd/lib/style/themes/variable.less'; +@import '../../../style/theme.less'; +@import '../../../../common/components/CommonParseSession/style/theme.less'; + +@prefix-cls: ~'@{chat-prefix}-top-msg'; + +.@{prefix-cls} { + &-wrap { + display: flex; + justify-content: space-between; + align-items: center; + margin: 8px 8px 0; + padding: 6px 16px; + background-color: @yx-primary-color; + border-radius: 8px; + border: 1px solid @yx-border-color-7; + color: @yx-primary-text-color; + font-size: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &-content { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 90%; + + .@{parse-session-cls} { + &-upload-img { + zoom: 0.1867; + } + + &-image { + .@{ant-prefix}-image-img { + zoom: 0.1867; + } + } + + &-video { + zoom: 0.1867; + } + + &-audio-container { + background-color: @yx-background-color-8; + padding: 0 10px; + border-radius: 8px; + } + + &-text-wrapper { + padding: 0; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + } + + &-location-card { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: auto; + + & > img { + display: none; + } + } + + &-location-title, + &-location-subTitle { + padding: 0; + color: @yx-primary-text-color; + } + + &-location-subTitle { + flex: 1; + text-wrap: wrap; + } + + &-mention { + color: @yx-primary-text-color; + } + } + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.ts new file mode 100644 index 0000000..703c2c2 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/chat/components/ChatTopMsg/style/index.ts @@ -0,0 +1,3 @@ +import 'antd/lib/button/style' + +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/chat/containers/p2pChatContainer.tsx b/react/src/YXUIKit/im-kit-ui/src/chat/containers/p2pChatContainer.tsx index 17cfefe..a5c4116 100644 --- a/react/src/YXUIKit/im-kit-ui/src/chat/containers/p2pChatContainer.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/chat/containers/p2pChatContainer.tsx @@ -15,12 +15,12 @@ import MessageInput, { ChatMessageInputRef, } from '../components/ChatMessageInput' import ChatSettingDrawer from '../components/ChatSettingDrawer' -import GroupCreate from '../components/ChatCreateTeam' import { ChatAction } from '../types' import { useStateContext, useTranslation, ComplexAvatarContainer, + CreateTeamModal, } from '../../common' import { Action, ChatSettingActionItem, MsgOperMenuItem } from '../Container' import ChatP2pSetting from '../components/ChatP2pSetting' @@ -30,14 +30,23 @@ import { message } from 'antd' import { storeConstants } from '@xkit-yx/im-store-v2' import { observer } from 'mobx-react' import ChatForwardModal from '../components/ChatForwardModal' -import { getImgDataUrl, getVideoFirstFrameDataUrl } from '../../utils' +import { getImgDataUrl, getVideoFirstFrameDataUrl, logger } from '../../utils' import { V2NIMConversationType, V2NIMConversation, } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMConversationService' import { V2NIMMessage } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMMessageService' -import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types' +import { + V2NIMMessageForUI, + YxReplyMsg, + YxServerExt, +} from '@xkit-yx/im-store-v2/dist/types/types' import { V2NIMConst } from 'nim-web-sdk-ng' +import { V2NIMError } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/types' +import { ChatAISearch } from '../components/ChatAISearch' +import { V2NIMFriend } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMFriendService' +import { ChatAITranslate } from '../components/ChatAITranslate' +import { MentionedMember } from '../components/ChatMessageInput/ChatMentionMemberList' export interface P2pChatContainerProps { conversationType: V2NIMConversationType @@ -104,14 +113,27 @@ const P2pChatContainer: React.FC = observer( // 当前输入框的回复消息 const replyMsg = store.msgStore.replyMsgs.get(conversationId) - const user = store.uiStore.getFriendWithUserNameCard(receiverId) + const { relation } = store.uiStore.getRelation(receiverId) + + const user = + relation === 'ai' + ? store.aiUserStore.aiUsers.get(receiverId) + : store.uiStore.getFriendWithUserNameCard(receiverId) const myUser = store.userStore.myUserInfo const userNickOrAccount = store.uiStore.getAppellation({ - account: user.accountId, + account: user?.accountId || '', }) + const mentionMembers = useMemo(() => { + if (relation === 'ai' || !localOptions.aiVisible) { + return [] + } + + return store.aiUserStore.getAIChatUser() + }, [store.aiUserStore, relation, localOptions.aiVisible]) + // TODO sdk 暂不支持用户在线状态 // const isOnline = store.eventStore.stateMap.get(receiverId) === 'online' const isOnline = 'online' @@ -142,6 +164,7 @@ const P2pChatContainer: React.FC = observer( const [forwardMessage, setForwardMessage] = useState< V2NIMMessageForUI | undefined >(undefined) + const [translateOpen, setTranslateOpen] = useState(false) const getHistory = useCallback( async (endTime: number, lastMsgId?: string) => { @@ -158,6 +181,7 @@ const P2pChatContainer: React.FC = observer( if (historyMsgs.length < storeConstants.HISTORY_LIMIT) { setNoMore(true) } + return historyMsgs } catch (error) { setLoadingMore(false) @@ -168,16 +192,18 @@ const P2pChatContainer: React.FC = observer( ) // 收消息,发消息时需要调用 - const scrollToBottom = useCallback( - debounce(() => { - if (messageListContainerDomRef.current) { - messageListContainerDomRef.current.scrollTop = - messageListContainerDomRef.current.scrollHeight - } - setReceiveMsgBtnVisible(false) - }, 300), - [] - ) + const scrollToBottom = useCallback(() => { + if (messageListContainerDomRef.current) { + messageListContainerDomRef.current.scrollTop = + messageListContainerDomRef.current.scrollHeight + } + + setReceiveMsgBtnVisible(false) + }, []) + + const onAISendHandler = useCallback(() => { + message.success(t('aiSendingText')) + }, [t]) const onMsgListScrollHandler = useCallback( debounce(async () => { @@ -204,6 +230,7 @@ const P2pChatContainer: React.FC = observer( ['beReCallMsg', 'reCallMsg'].includes(item.recallType || '') ) )[0] + if (_msg) { await getHistory(_msg.createTime, _msg.messageServerId) // 滚动到加载的那条消息 @@ -220,9 +247,11 @@ const P2pChatContainer: React.FC = observer( const settingAction = settingActions?.find( (item) => item.action === action ) + if (settingAction?.onClick) { return settingAction?.onClick() } + switch (action) { case 'chatSetting': setAction(action) @@ -244,10 +273,52 @@ const P2pChatContainer: React.FC = observer( (msg: V2NIMMessageForUI) => { setInputValue(msg.oldText || '') const replyMsg = replyMsgsMap[msg.messageClientId] + replyMsg && store.msgStore.replyMsgActive(replyMsg) + // 处理 @ 消息 + const { serverExtension } = msg + + if (serverExtension) { + try { + const extObj: YxServerExt = JSON.parse(serverExtension) + const yxAitMsg = extObj.yxAitMsg + + if (yxAitMsg) { + const _mentionedMembers: MentionedMember[] = [] + + Object.keys(yxAitMsg).forEach((key) => { + if (key === storeConstants.AT_ALL_ACCOUNT) { + _mentionedMembers.push({ + account: storeConstants.AT_ALL_ACCOUNT, + appellation: t('teamAll'), + }) + } else { + const member = mentionMembers.find( + (item) => item.accountId === key + ) + + member && + _mentionedMembers.push({ + account: member.accountId, + appellation: store.uiStore.getAppellation({ + account: member.accountId, + ignoreAlias: true, + }), + }) + } + }) + chatMessageInputRef.current?.setSelectedAtMembers( + _mentionedMembers + ) + } + } catch { + // + } + } + chatMessageInputRef.current?.input?.focus() }, - [replyMsgsMap, store.msgStore] + [replyMsgsMap, mentionMembers, store.msgStore, store.uiStore, t] ) const onResend = useCallback( @@ -263,6 +334,17 @@ const P2pChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, + }) + break + case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT: + await store.msgStore.sendMessageActive({ + msg, + conversationId, + sendBefore: () => { + scrollToBottom() + }, + onAISend: onAISendHandler, }) break default: @@ -272,19 +354,21 @@ const P2pChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) break } + scrollToBottom() } catch (error) { // } }, - [store.msgStore, conversationId, scrollToBottom] + [store.msgStore, conversationId, scrollToBottom, onAISendHandler] ) const onSendText = useCallback( - async (value: string) => { + async (value: string, ext?: YxServerExt) => { try { if (onSendTextFromProps) { await onSendTextFromProps({ @@ -294,12 +378,15 @@ const P2pChatContainer: React.FC = observer( }) } else { const textMsg = nim.V2NIMMessageCreator.createTextMessage(value) + await store.msgStore.sendMessageActive({ msg: textMsg, conversationId, + serverExtension: ext as Record, sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } } catch (error) { @@ -316,6 +403,7 @@ const P2pChatContainer: React.FC = observer( conversationId, scrollToBottom, nim.V2NIMMessageCreator, + onAISendHandler, ] ) @@ -326,12 +414,14 @@ const P2pChatContainer: React.FC = observer( file, file.name ) + await store.msgStore.sendMessageActive({ msg: fileMsg, conversationId, sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -339,7 +429,13 @@ const P2pChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onSendImg = useCallback( @@ -347,6 +443,7 @@ const P2pChatContainer: React.FC = observer( try { const previewImg = await getImgDataUrl(file) const imgMsg = nim.V2NIMMessageCreator.createImageMessage(file) + await store.msgStore.sendMessageActive({ msg: imgMsg, conversationId, @@ -355,6 +452,7 @@ const P2pChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -362,7 +460,13 @@ const P2pChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onSendVideo = useCallback( @@ -370,6 +474,7 @@ const P2pChatContainer: React.FC = observer( try { const previewImg = await getVideoFirstFrameDataUrl(file) const videoMsg = nim.V2NIMMessageCreator.createVideoMessage(file) + await store.msgStore.sendMessageActive({ msg: videoMsg, conversationId, @@ -378,6 +483,7 @@ const P2pChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -385,7 +491,13 @@ const P2pChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onRemoveReplyMsg = useCallback(() => { @@ -395,9 +507,11 @@ const P2pChatContainer: React.FC = observer( const onMessageAction = useCallback( async (key: MenuItemKey, msg: V2NIMMessageForUI) => { const msgOperMenuItem = msgOperMenu?.find((item) => item.key === key) + if (msgOperMenuItem?.onClick) { return msgOperMenuItem?.onClick(msg) } + switch (key) { case 'delete': await store.msgStore.deleteMsgActive([msg]) @@ -406,8 +520,44 @@ const P2pChatContainer: React.FC = observer( await store.msgStore.reCallMsgActive(msg) break case 'reply': - store.msgStore.replyMsgActive(msg) - chatMessageInputRef.current?.input?.focus() + { + // const member = mentionMembers.find( + // (item) => item.accountId === msg.senderId + // ) + + // member && + // chatMessageInputRef.current?.onAtMemberSelectHandler({ + // account: member.accountId, + // appellation: store.uiStore.getAppellation({ + // account: member.accountId, + // ignoreAlias: true, + // }), + // }) + store.msgStore.replyMsgActive(msg) + chatMessageInputRef.current?.input?.focus() + } + + break + case 'collection': + try { + await nim.V2NIMMessageService.addCollection({ + collectionType: msg.messageType + 1000, + collectionData: JSON.stringify({ + message: nim.V2NIMMessageConverter.messageSerialization(msg), + conversationName: conversation?.name, + senderName: store.uiStore.getAppellation({ + account: msg.senderId, + }), + avatar: store.userStore.users.get(msg.senderId)?.avatar, + }), + uniqueId: msg.messageServerId, + }) + message.success(t('collectionSuccess')) + } catch (error: unknown) { + message.error(t('collectionFailed')) + logger.error('收藏失败:', (error as V2NIMError).toString()) + } + break case 'forward': setForwardMessage(msg) @@ -416,33 +566,48 @@ const P2pChatContainer: React.FC = observer( break } }, - [msgOperMenu, store.msgStore] + [ + msgOperMenu, + mentionMembers, + store.msgStore, + store.userStore.users, + store.uiStore, + conversation?.name, + nim.V2NIMMessageConverter, + nim.V2NIMMessageService, + t, + ] ) - const onGroupCreate = useCallback( - async ({ - name, - avatar, - selectedAccounts, - }: { - name: string - avatar: string - selectedAccounts: string[] - }) => { - try { - await store.teamStore.createTeamActive({ - name, - avatar, - accounts: selectedAccounts, - }) - resetSettingState() - message.success(t('createTeamSuccessText')) - } catch (error: any) { - message.error(t('createTeamFailedText')) + const createAIWelcomeMsg = useCallback(() => { + const aiExt = store.aiUserStore.getAIUserServerExt(receiverId) + + if (aiExt.welcomeText) { + const welcomeMsg = nim.V2NIMMessageCreator.createTextMessage( + aiExt.welcomeText || 'hello' + ) + + welcomeMsg.senderId = receiverId + welcomeMsg.receiverId = store.userStore.myUserInfo.accountId + welcomeMsg.conversationId = conversationId + welcomeMsg.sendingState = + V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED + welcomeMsg.isSelf = false + welcomeMsg.aiConfig = { + accountId: receiverId, + aiStatus: 2, } - }, - [store.teamStore, t] - ) + + store.msgStore.addMsg(conversationId, [welcomeMsg]) + } + }, [ + conversationId, + nim.V2NIMMessageCreator, + receiverId, + store.aiUserStore, + store.msgStore, + store.userStore.myUserInfo.accountId, + ]) const resetSettingState = () => { setAction(undefined) @@ -457,7 +622,9 @@ const P2pChatContainer: React.FC = observer( setNoMore(false) setReceiveMsgBtnVisible(false) setForwardMessage(undefined) - }, []) + setTranslateOpen(false) + store.aiUserStore.resetAIProxy() + }, [store.aiUserStore]) const handleForwardModalSend = () => { scrollToBottom() @@ -468,6 +635,10 @@ const P2pChatContainer: React.FC = observer( setForwardMessage(undefined) } + const handleCreateModalClose = () => { + setGroupCreateVisible(false) + } + useEffect(() => { const notMyMsgs = msgs .filter((item) => item.senderId !== myUser.accountId) @@ -491,10 +662,16 @@ const P2pChatContainer: React.FC = observer( const msg = notMyMsgs.find( (item) => item.messageClientId === params.target.id ) + if (msg) { - store.msgStore.sendMsgReceiptActive(msg).finally(() => { - visibilityObserver.unobserve(params.target) - }) + store.msgStore + .sendMsgReceiptActive(msg) + .catch(() => { + // 忽略这个报错 + }) + .finally(() => { + visibilityObserver.unobserve(params.target) + }) } } } @@ -502,6 +679,7 @@ const P2pChatContainer: React.FC = observer( const handler = (isObserve: boolean) => { notMyMsgs.forEach((item) => { const target = document.getElementById(item.messageClientId) + if (target) { if (isObserve) { visibilityObserver.observe(target) @@ -550,6 +728,10 @@ const P2pChatContainer: React.FC = observer( if (memoryMsgs.length < 10) { getHistory(Date.now()).then((res) => { + if (!res?.length && !memoryMsgs.length && relation === 'ai') { + createAIWelcomeMsg() + } + scrollToBottom() // TODO 考虑以下这段代码是否还需要 // if (conversation && !conversation.lastMessage && res && res[0]) { @@ -561,69 +743,99 @@ const P2pChatContainer: React.FC = observer( } else { scrollToBottom() } - }, [store.msgStore, conversationId, getHistory, scrollToBottom]) + }, [ + store.msgStore, + conversationId, + relation, + getHistory, + scrollToBottom, + createAIWelcomeMsg, + ]) // 处理消息 useEffect(() => { if (msgs.length !== 0) { const replyMsgsMap = {} - const reqMsgs: Array<{ - scene: 'p2p' | 'team' - from: string - to: string - idServer: string - idClient: string - time: number - }> = [] + const reqMsgs: YxReplyMsg[] = [] const messageClientIds: Record = {} + msgs.forEach((msg) => { if (msg.serverExtension) { try { - const { yxReplyMsg } = JSON.parse(msg.serverExtension) + const { yxReplyMsg } = JSON.parse( + msg.serverExtension + ) as YxServerExt + if (yxReplyMsg) { const replyMsg = msgs.find( (item) => item.messageClientId === yxReplyMsg.idClient ) + if (replyMsg) { replyMsgsMap[msg.messageClientId] = replyMsg } else { replyMsgsMap[msg.messageClientId] = 'noFind' - const { scene, from, to, idServer, idClient, time } = - yxReplyMsg - if (scene && from && to && idServer && idClient && time) { - reqMsgs.push({ scene, from, to, idServer, idClient, time }) + const { + scene, + from, + to, + idServer, + idClient, + time, + receiverId, + } = yxReplyMsg + + if ( + scene && + from && + to && + idServer && + idClient && + time && + receiverId + ) { + reqMsgs.push({ + scene, + from, + to, + idServer, + idClient, + time, + receiverId, + }) messageClientIds[idServer] = msg.messageClientId } } } - } catch {} + } catch { + // + } } }) if (reqMsgs.length > 0) { nim.V2NIMMessageService.getMessageListByRefers( reqMsgs.map((item) => ({ senderId: item.from, - receiverId: item.to, + receiverId: item.receiverId, messageClientId: item.idClient, messageServerId: item.idServer, createTime: item.time, - conversationType: - item.scene === 'p2p' - ? V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P - : V2NIMConst.V2NIMConversationType - .V2NIM_CONVERSATION_TYPE_TEAM, - conversationId: nim.V2NIMConversationIdUtil.p2pConversationId( - item.to - ), + conversationType: item.scene, + conversationId: item.to, })) - ).then((res) => { - res.forEach((item) => { - if (item.messageServerId) { - replyMsgsMap[messageClientIds[item.messageServerId]] = item - } + ) + .then((res) => { + res.forEach((item) => { + if (item.messageServerId) { + replyMsgsMap[messageClientIds[item.messageServerId]] = + store.msgStore.handleReceiveAIMsg(item) + } + }) + setReplyMsgsMap({ ...replyMsgsMap }) + }) + .catch((err) => { + logger.error('获取回复消息失败:', (err as V2NIMError).toString()) }) - setReplyMsgsMap({ ...replyMsgsMap }) - }) } else { setReplyMsgsMap({ ...replyMsgsMap }) } @@ -708,7 +920,16 @@ const P2pChatContainer: React.FC = observer( renderMessageInnerContent={renderMessageInnerContent} renderMessageOuterContent={renderMessageOuterContent} /> - + { + setTranslateOpen(false) + }} + prefix={prefix} + inputValue={inputValue} + visible={translateOpen} + setInputValue={setInputValue} + /> = observer( ? renderP2pInputPlaceHolder(conversation) : `${t('sendToText')} ${userNickOrAccount}${t('sendUsageText')}` } + translateOpen={translateOpen} + onTranslate={setTranslateOpen} replyMsg={replyMsg} + mentionMembers={mentionMembers} conversationType={conversationType} receiverId={receiverId} actions={actions} inputValue={inputValue} setInputValue={setInputValue} + allowAtAll={false} onSendText={onSendText} onSendFile={onSendFile} onSendImg={onSendImg} onSendVideo={onSendVideo} onRemoveReplyMsg={onRemoveReplyMsg} /> + = observer( title={t('setText')} > { setGroupCreateVisible(true) }} @@ -752,15 +978,13 @@ const P2pChatContainer: React.FC = observer( settingActions={settingActions} onActionClick={onActionClick} /> - { - setGroupCreateVisible(false) - }} - prefix={prefix} - commonPrefix={commonPrefix} + // onAfterCreate={resetSettingState} + onChat={handleCreateModalClose} + onCancel={handleCreateModalClose} + prefix={commonPrefix} /> = observer( prefix = 'chat', commonPrefix = 'common', }) => { - const { store, nim } = useStateContext() + const { store, nim, localOptions } = useStateContext() const { t } = useTranslation() @@ -185,14 +192,28 @@ const TeamChatContainer: React.FC = observer( ) .sort((a, b) => a.joinTime - b.joinTime) const _sortedMembers = [...owner, ...manager, ...other] + return _sortedMembers }, [teamMembers]) const mentionMembers = useMemo(() => { - return sortedMembers.filter( - (member) => member.accountId !== myUser?.accountId - ) - }, [sortedMembers, myUser?.accountId]) + const aiChatUser = localOptions.aiVisible + ? store.aiUserStore.getAIChatUser() + : [] + + const sortedMembersWithoutMeAndAI = sortedMembers + .filter((item) => item.accountId !== myUser?.accountId) + .filter((item) => + aiChatUser.every((aiUser) => aiUser.accountId !== item.accountId) + ) + + return [...aiChatUser, ...sortedMembersWithoutMeAndAI] + }, [ + sortedMembers, + myUser?.accountId, + store.aiUserStore, + localOptions.aiVisible, + ]) const teamMute = useMemo(() => { if ( @@ -202,21 +223,29 @@ const TeamChatContainer: React.FC = observer( ) { return !isGroupOwner && !isGroupManager } + return false }, [team.chatBannedMode, isGroupOwner, isGroupManager]) - const allowAtAll = useMemo(() => { - let ext: TAllowAt = {} + const serverExt = useMemo(() => { + let ext: YxServerExt = {} + try { ext = JSON.parse(team.serverExtension || '{}') } catch (error) { // } - if (ext[ALLOW_AT] === 'manager') { + + return ext + }, [team.serverExtension]) + + const allowAtAll = useMemo(() => { + if (serverExt.yxAllowAt === 'manager') { return isGroupOwner || isGroupManager } + return true - }, [team.serverExtension, isGroupOwner, isGroupManager]) + }, [serverExt, isGroupOwner, isGroupManager]) const teamDefaultAddMembers = useMemo(() => { return teamMembers @@ -252,6 +281,7 @@ const TeamChatContainer: React.FC = observer( const [forwardMessage, setForwardMessage] = useState< V2NIMMessageForUI | undefined >(undefined) + const [translateOpen, setTranslateOpen] = useState(false) const SETTING_NAV_TITLE_MAP: { [key in ChatAction]: string } = useMemo( () => ({ @@ -263,6 +293,7 @@ const TeamChatContainer: React.FC = observer( const title = useMemo(() => { const defaultTitle = SETTING_NAV_TITLE_MAP[action || 'chatSetting'] + if (navHistoryStack.length > 1) { return ( = observer( ) } + return {defaultTitle} }, [navHistoryStack, SETTING_NAV_TITLE_MAP, action]) @@ -292,14 +324,16 @@ const TeamChatContainer: React.FC = observer( lastMsgId, limit: storeConstants.HISTORY_LIMIT, }) + setLoadingMore(false) if (historyMsgs.length < storeConstants.HISTORY_LIMIT) { setNoMore(true) } + return historyMsgs - } catch (error: any) { + } catch (error) { setLoadingMore(false) - switch (error.code) { + switch ((error as V2NIMError)?.code) { case 109404: message.error(t('teamMemberNotExist')) break @@ -313,16 +347,18 @@ const TeamChatContainer: React.FC = observer( ) // 收消息,发消息时需要调用 - const scrollToBottom = useCallback( - debounce(() => { - if (messageListContainerDomRef.current) { - messageListContainerDomRef.current.scrollTop = - messageListContainerDomRef.current.scrollHeight - } - setReceiveMsgBtnVisible(false) - }, 300), - [] - ) + const scrollToBottom = useCallback(() => { + if (messageListContainerDomRef.current) { + messageListContainerDomRef.current.scrollTop = + messageListContainerDomRef.current.scrollHeight + } + + setReceiveMsgBtnVisible(false) + }, []) + + const onAISendHandler = useCallback(() => { + message.success(t('aiSendingText')) + }, [t]) const onMsgListScrollHandler = useCallback( debounce(async () => { @@ -349,6 +385,7 @@ const TeamChatContainer: React.FC = observer( ['beReCallMsg', 'reCallMsg'].includes(item.recallType || '') ) )[0] + if (_msg) { await getHistory(_msg.createTime, _msg.messageServerId) // 滚动到加载的那条消息 @@ -365,9 +402,11 @@ const TeamChatContainer: React.FC = observer( const settingAction = settingActions?.find( (item) => item.action === action ) + if (settingAction?.onClick) { return settingAction?.onClick() } + switch (action) { case 'chatSetting': setAction(action) @@ -389,46 +428,62 @@ const TeamChatContainer: React.FC = observer( const onReeditClick = useCallback( (msg: V2NIMMessageForUI) => { const replyMsg = replyMsgsMap[msg.messageClientId] + replyMsg && store.msgStore.replyMsgActive(replyMsg) // 处理 @ 消息 const { serverExtension } = msg + if (serverExtension) { try { - const extObj = JSON.parse(serverExtension) + const extObj: YxServerExt = JSON.parse(serverExtension) const yxAitMsg = extObj.yxAitMsg + if (yxAitMsg) { - const mentionedMembers: MentionedMember[] = [] + const _mentionedMembers: MentionedMember[] = [] + Object.keys(yxAitMsg).forEach((key) => { if (key === storeConstants.AT_ALL_ACCOUNT) { - mentionedMembers.push({ + _mentionedMembers.push({ account: storeConstants.AT_ALL_ACCOUNT, appellation: t('teamAll'), }) } else { - const member = teamMembers.find( + const member = mentionMembers.find( (item) => item.accountId === key ) + member && - mentionedMembers.push({ + _mentionedMembers.push({ account: member.accountId, appellation: store.uiStore.getAppellation({ account: member.accountId, - teamId: member.teamId, + teamId: + (member as V2NIMTeamMember).teamId || team.teamId, ignoreAlias: true, }), }) } }) chatMessageInputRef.current?.setSelectedAtMembers( - mentionedMembers + _mentionedMembers ) } - } catch {} + } catch { + // + } } + setInputValue(msg.oldText || '') chatMessageInputRef.current?.input?.focus() }, - [replyMsgsMap, store.msgStore, teamMembers, store.uiStore, t] + [ + replyMsgsMap, + store.msgStore, + mentionMembers, + team.teamId, + store.uiStore, + t, + ] ) const onResend = useCallback( @@ -444,6 +499,7 @@ const TeamChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) break default: @@ -453,19 +509,21 @@ const TeamChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) break } + scrollToBottom() } catch (error) { // } }, - [store.msgStore, conversationId, scrollToBottom] + [store.msgStore, conversationId, scrollToBottom, onAISendHandler] ) const onSendText = useCallback( - async (value: string, ext?: Record) => { + async (value: string, ext?: YxServerExt) => { try { if (onSendTextFromProps) { await onSendTextFromProps({ @@ -475,13 +533,15 @@ const TeamChatContainer: React.FC = observer( }) } else { const textMsg = nim.V2NIMMessageCreator.createTextMessage(value) + await store.msgStore.sendMessageActive({ msg: textMsg, conversationId, - serverExtension: ext, + serverExtension: ext as Record, sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } } catch (error) { @@ -498,6 +558,7 @@ const TeamChatContainer: React.FC = observer( conversationId, scrollToBottom, nim.V2NIMMessageCreator, + onAISendHandler, ] ) @@ -508,12 +569,14 @@ const TeamChatContainer: React.FC = observer( file, file.name ) + await store.msgStore.sendMessageActive({ msg: fileMsg, conversationId, sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -521,7 +584,13 @@ const TeamChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onSendImg = useCallback( @@ -529,6 +598,7 @@ const TeamChatContainer: React.FC = observer( try { const previewImg = await getImgDataUrl(file) const imgMsg = nim.V2NIMMessageCreator.createImageMessage(file) + await store.msgStore.sendMessageActive({ msg: imgMsg, conversationId, @@ -537,6 +607,7 @@ const TeamChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -544,7 +615,13 @@ const TeamChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onSendVideo = useCallback( @@ -552,6 +629,7 @@ const TeamChatContainer: React.FC = observer( try { const previewImg = await getVideoFirstFrameDataUrl(file) const videoMsg = nim.V2NIMMessageCreator.createVideoMessage(file) + await store.msgStore.sendMessageActive({ msg: videoMsg, conversationId, @@ -560,6 +638,7 @@ const TeamChatContainer: React.FC = observer( sendBefore: () => { scrollToBottom() }, + onAISend: onAISendHandler, }) } catch (error) { // message.error(t('sendMsgFailedText')) @@ -567,19 +646,95 @@ const TeamChatContainer: React.FC = observer( scrollToBottom() } }, - [store.msgStore, conversationId, scrollToBottom, nim.V2NIMMessageCreator] + [ + store.msgStore, + conversationId, + scrollToBottom, + nim.V2NIMMessageCreator, + onAISendHandler, + ] ) const onRemoveReplyMsg = useCallback(() => { replyMsg && store.msgStore.removeReplyMsgActive(replyMsg.conversationId) }, [replyMsg, store.msgStore]) + // 默认群主和管理员 + const allowTop = useMemo(() => { + if (serverExt.im_ui_kit_group) { + return true + } + + if (serverExt.yxAllowTop === 'all') { + return true + } + + return isGroupOwner || isGroupManager + }, [serverExt, isGroupOwner, isGroupManager]) + + const handleTopMessage = useCallback( + async (msg: V2NIMMessageForUI, isTop: boolean) => { + if (!allowTop) { + message.error(t('noPermission')) + return + } + + const serverExtension = { ...serverExt } + + serverExtension.lastOpt = 'yxMessageTop' + + const _msg = store.msgStore.handleMsgForSDK(msg) + + serverExtension.yxMessageTop = { + idClient: _msg.messageClientId, + scene: _msg.conversationType, + idServer: _msg.messageServerId, + from: _msg.senderId, + receiverId: _msg.receiverId, + to: _msg.conversationId, + time: _msg.createTime, + operator: myUser.accountId, + operation: isTop ? 0 : 1, + } + try { + await store.teamStore.updateTeamActive({ + teamId: team.teamId, + info: { + serverExtension: JSON.stringify(serverExtension), + }, + }) + } catch (error) { + logger.error('top message failed: ', (error as V2NIMError).toString()) + switch ((error as V2NIMError)?.code) { + // 无权限 + case 109432: + message.error(t('noPermission')) + break + default: + message.error(t('topFailedText')) + break + } + } + }, + [ + store.teamStore, + store.msgStore, + myUser.accountId, + t, + team.teamId, + allowTop, + serverExt, + ] + ) + const onMessageAction = useCallback( async (key: MenuItemKey, msg: V2NIMMessageForUI) => { const msgOperMenuItem = msgOperMenu?.find((item) => item.key === key) + if (msgOperMenuItem?.onClick) { return msgOperMenuItem?.onClick(msg) } + switch (key) { case 'delete': await store.msgStore.deleteMsgActive([msg]) @@ -588,47 +743,95 @@ const TeamChatContainer: React.FC = observer( await store.msgStore.reCallMsgActive(msg) break case 'reply': - const member = mentionMembers.find( - (item) => item.accountId === msg.senderId - ) - member && - chatMessageInputRef.current?.onAtMemberSelectHandler({ - account: member.accountId, - appellation: store.uiStore.getAppellation({ + { + const member = mentionMembers.find( + (item) => item.accountId === msg.senderId + ) + + member && + chatMessageInputRef.current?.onAtMemberSelectHandler({ account: member.accountId, - teamId: member.teamId, - ignoreAlias: true, + appellation: store.uiStore.getAppellation({ + account: member.accountId, + teamId: (member as V2NIMTeamMember).teamId, + ignoreAlias: true, + }), + }) + store.msgStore.replyMsgActive(msg) + chatMessageInputRef.current?.input?.focus() + } + + break + case 'collection': + try { + await nim.V2NIMMessageService.addCollection({ + collectionType: msg.messageType + 1000, + collectionData: JSON.stringify({ + message: nim.V2NIMMessageConverter.messageSerialization(msg), + conversationName: conversation?.name, + senderName: store.uiStore.getAppellation({ + account: msg.senderId, + teamId: team.teamId, + }), + avatar: store.userStore.users.get(msg.senderId)?.avatar, }), + uniqueId: msg.messageServerId, }) - store.msgStore.replyMsgActive(msg) - chatMessageInputRef.current?.input?.focus() + message.success(t('collectionSuccess')) + } catch (error: unknown) { + message.error(t('collectionFailed')) + logger.error('收藏失败:', (error as V2NIMError).toString()) + } + break case 'forward': setForwardMessage(msg) break + case 'top': + handleTopMessage(msg, true) + break + case 'unTop': + handleTopMessage(msg, false) + break default: break } }, - [store.msgStore, mentionMembers, store.uiStore, msgOperMenu] + [ + store.msgStore, + conversation?.name, + nim.V2NIMMessageConverter, + store.userStore.users, + team.teamId, + t, + mentionMembers, + store.uiStore, + nim.V2NIMMessageService, + handleTopMessage, + msgOperMenu, + ] ) const onMessageAvatarAction = useCallback( async (key: AvatarMenuItem, msg: V2NIMMessageForUI) => { switch (key) { case 'mention': - const member = mentionMembers.find( - (item) => item.accountId === msg.senderId - ) - member && - chatMessageInputRef.current?.onAtMemberSelectHandler({ - account: member.accountId, - appellation: store.uiStore.getAppellation({ + { + const member = mentionMembers.find( + (item) => item.accountId === msg.senderId + ) + + member && + chatMessageInputRef.current?.onAtMemberSelectHandler({ account: member.accountId, - teamId: member.teamId, - ignoreAlias: true, - }), - }) + appellation: store.uiStore.getAppellation({ + account: member.accountId, + teamId: (member as V2NIMTeamMember).teamId, + ignoreAlias: true, + }), + }) + } + break default: break @@ -641,8 +844,8 @@ const TeamChatContainer: React.FC = observer( try { await store.teamStore.dismissTeamActive(team.teamId) message.success(t('dismissTeamSuccessText')) - } catch (error: any) { - switch (error?.code) { + } catch (error) { + switch ((error as V2NIMError)?.code) { // 无权限 case 109427: message.error(t('noPermission')) @@ -658,7 +861,7 @@ const TeamChatContainer: React.FC = observer( try { await store.teamStore.leaveTeamActive(team.teamId) message.success(t('leaveTeamSuccessText')) - } catch (error: any) { + } catch (error) { message.error(t('leaveTeamFailedText')) } }, [store.teamStore, team.teamId, t]) @@ -694,8 +897,8 @@ const TeamChatContainer: React.FC = observer( }) message.success(t('addTeamMemberSuccessText')) resetSettingState() - } catch (error: any) { - switch (error?.code) { + } catch (error) { + switch ((error as V2NIMError)?.code) { // 无权限 case 109306: message.error(t('noPermission')) @@ -717,8 +920,8 @@ const TeamChatContainer: React.FC = observer( accounts: [member.accountId], }) message.success(t('removeTeamMemberSuccessText')) - } catch (error: any) { - switch (error?.code) { + } catch (error) { + switch ((error as V2NIMError)?.code) { // 无权限 case 109306: message.error(t('noPermission')) @@ -740,8 +943,8 @@ const TeamChatContainer: React.FC = observer( info: params, }) message.success(t('updateTeamSuccessText')) - } catch (error: any) { - switch (error?.code) { + } catch (error) { + switch ((error as V2NIMError)?.code) { // 无权限 case 109432: message.error(t('noPermission')) @@ -758,6 +961,7 @@ const TeamChatContainer: React.FC = observer( const onUpdateMyMemberInfo = useCallback( async (params: V2NIMUpdateSelfMemberInfoParams) => { const nickTipVisible = params.teamNick !== void 0 + try { await store.teamMemberStore.updateMyMemberInfoActive({ teamId: team.teamId, @@ -766,7 +970,7 @@ const TeamChatContainer: React.FC = observer( if (nickTipVisible) { message.success(t('updateMyMemberNickSuccess')) } - } catch (error: any) { + } catch (error) { if (nickTipVisible) { message.error(t('updateMyMemberNickFailed')) } @@ -789,8 +993,8 @@ const TeamChatContainer: React.FC = observer( message.success( mute ? t('muteAllTeamSuccessText') : t('unmuteAllTeamSuccessText') ) - } catch (error: any) { - switch (error?.code) { + } catch (error) { + switch ((error as V2NIMError)?.code) { // 无权限 case 109432: message.error(t('noPermission')) @@ -820,7 +1024,9 @@ const TeamChatContainer: React.FC = observer( setNoMore(false) setReceiveMsgBtnVisible(false) setForwardMessage(undefined) - }, []) + setTranslateOpen(false) + store.aiUserStore.resetAIProxy() + }, [store.aiUserStore]) const handleForwardModalSend = () => { scrollToBottom() @@ -858,10 +1064,11 @@ const TeamChatContainer: React.FC = observer( const msg = notMyMsgs.find( (item) => item.messageClientId === params.target.id ) + if (msg) { store.msgStore .sendTeamMsgReceiptActive([msg]) - .catch((err) => { + .catch(() => { // 忽略这个报错 }) .finally(() => { @@ -874,6 +1081,7 @@ const TeamChatContainer: React.FC = observer( const handler = (isObserve: boolean) => { notMyMsgs.forEach((item) => { const target = document.getElementById(item.messageClientId) + if (target) { if (isObserve) { visibilityObserver.observe(target) @@ -914,7 +1122,7 @@ const TeamChatContainer: React.FC = observer( resetState() scrollToBottom() store.teamStore.getTeamActive(receiverId).catch((err) => { - console.warn('获取群组失败:', err.toString()) + logger.warn('获取群组失败:', err.toString()) }) store.teamMemberStore .getTeamMemberActive({ @@ -926,7 +1134,7 @@ const TeamChatContainer: React.FC = observer( }, }) .catch((err) => { - console.warn('获取群组成员失败:', err.toString()) + logger.warn('获取群组成员失败:', err.toString()) }) }, [ team.memberLimit, @@ -949,7 +1157,7 @@ const TeamChatContainer: React.FC = observer( : [] if (memoryMsgs.length < 10) { - getHistory(Date.now()).then((res) => { + getHistory(Date.now()).then(() => { scrollToBottom() // TODO 考虑以下这段代码是否还需要 // if (conversation && !conversation.lastMessage && res && res[0]) { @@ -963,6 +1171,7 @@ const TeamChatContainer: React.FC = observer( const myMsgs = memoryMsgs.filter( (item) => item.senderId === myUser.accountId ) + // 获取群组已读未读数 store.msgStore.getTeamMsgReadsActive(myMsgs, conversationId) scrollToBottom() @@ -979,61 +1188,84 @@ const TeamChatContainer: React.FC = observer( useEffect(() => { if (msgs.length !== 0) { const replyMsgsMap = {} - const reqMsgs: Array<{ - scene: 'p2p' | 'team' - from: string - to: string - idServer: string - idClient: string - time: number - }> = [] + const reqMsgs: YxReplyMsg[] = [] const messageClientIds: string[] = [] + msgs.forEach((msg) => { if (msg.serverExtension) { try { - const { yxReplyMsg } = JSON.parse(msg.serverExtension) + const { yxReplyMsg } = JSON.parse( + msg.serverExtension + ) as YxServerExt + if (yxReplyMsg) { const replyMsg = msgs.find( (item) => item.messageClientId === yxReplyMsg.idClient ) + if (replyMsg) { replyMsgsMap[msg.messageClientId] = replyMsg } else { replyMsgsMap[msg.messageClientId] = 'noFind' - const { scene, from, to, idServer, idClient, time } = - yxReplyMsg - if (scene && from && to && idServer && idClient && time) { - reqMsgs.push({ scene, from, to, idServer, idClient, time }) + const { + scene, + from, + to, + idServer, + idClient, + time, + receiverId, + } = yxReplyMsg + + if ( + scene && + from && + to && + idServer && + idClient && + time && + receiverId + ) { + reqMsgs.push({ + scene, + from, + to, + idServer, + idClient, + time, + receiverId, + }) messageClientIds.push(msg.messageClientId) } } } - } catch {} + } catch { + // + } } }) if (reqMsgs.length > 0) { nim.V2NIMMessageService.getMessageListByRefers( reqMsgs.map((item) => ({ senderId: item.from, - receiverId: item.to, + receiverId: item.receiverId, messageClientId: item.idClient, messageServerId: item.idServer, createTime: item.time, - conversationType: - item.scene === 'p2p' - ? V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P - : V2NIMConst.V2NIMConversationType - .V2NIM_CONVERSATION_TYPE_TEAM, - conversationId: nim.V2NIMConversationIdUtil.teamConversationId( - item.to - ), + conversationType: item.scene, + conversationId: item.to, })) - ).then((res) => { - res.forEach((item, index) => { - replyMsgsMap[messageClientIds[index]] = item + ) + .then((res) => { + res.forEach((item, index) => { + replyMsgsMap[messageClientIds[index]] = + store.msgStore.handleReceiveAIMsg(item) + }) + setReplyMsgsMap({ ...replyMsgsMap }) + }) + .catch((err) => { + logger.error('获取回复消息失败:', (err as V2NIMError).toString()) }) - setReplyMsgsMap({ ...replyMsgsMap }) - }) } else { setReplyMsgsMap({ ...replyMsgsMap }) } @@ -1077,6 +1309,7 @@ const TeamChatContainer: React.FC = observer( // 根据 onMsg 处理提示 const onMsgToast = (msgs: V2NIMMessageForUI[]) => { const msg = msgs[0] + if ( msg.conversationId === conversationId && msg.messageType === @@ -1084,6 +1317,7 @@ const TeamChatContainer: React.FC = observer( ) { const attachment = msg.attachment as V2NIMMessageNotificationAttachment + switch (attachment?.type) { // 主动离开群聊 case V2NIMConst.V2NIMMessageNotificationType @@ -1098,8 +1332,10 @@ const TeamChatContainer: React.FC = observer( })}${t('leaveTeamText')}` ) } + break } + // 踢出群聊 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: { @@ -1112,10 +1348,13 @@ const TeamChatContainer: React.FC = observer( teamId: msg.receiverId, }) ) + message.info(`${nicks.join(',')}${t('leaveTeamText')}`) } + break } + // 解散群聊 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS: @@ -1124,6 +1363,18 @@ const TeamChatContainer: React.FC = observer( // 有人主动加入群聊 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS: + { + if (msg.senderId === myUser.accountId) { + message.info( + `${store.uiStore.getAppellation({ + account: msg.senderId, + teamId: msg.receiverId, + })}${t('enterTeamText')}` + ) + } + } + + break // 邀请加入群聊对方同意 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: @@ -1137,6 +1388,7 @@ const TeamChatContainer: React.FC = observer( ) } } + break // 邀请加入群聊无需验证 case V2NIMConst.V2NIMMessageNotificationType @@ -1147,6 +1399,7 @@ const TeamChatContainer: React.FC = observer( teamId: msg.receiverId, }) ) + message.info(`${nicks.join(',')}${t('enterTeamText')}`) break } @@ -1180,6 +1433,15 @@ const TeamChatContainer: React.FC = observer( } /> )} + {serverExt.yxMessageTop?.operation === 0 ? ( + + ) : null} = observer( msgs={msgs} msgOperMenu={msgOperMenu} replyMsgsMap={replyMsgsMap} + topMessage={serverExt.yxMessageTop} members={teamMembers} noMore={noMore} loadingMore={loadingMore} @@ -1203,7 +1466,16 @@ const TeamChatContainer: React.FC = observer( renderMessageInnerContent={renderMessageInnerContent} renderMessageOuterContent={renderMessageOuterContent} /> - + { + setTranslateOpen(false) + }} + prefix={prefix} + inputValue={inputValue} + visible={translateOpen} + setInputValue={setInputValue} + /> = observer( ? t('teamMutePlaceholder') : `${t('sendToText')} ${teamNameOrTeamId}${t('sendUsageText')}` } + translateOpen={translateOpen} + onTranslate={setTranslateOpen} replyMsg={replyMsg} mentionMembers={mentionMembers} conversationType={conversationType} @@ -1233,6 +1507,7 @@ const TeamChatContainer: React.FC = observer( onSendImg={onSendImg} onSendVideo={onSendVideo} /> + = observer( onActionClick={onActionClick} /> void } export const pauseAllAudio = (): HTMLAudioElement => { const audio = document.getElementById('yx-audio-message') as HTMLAudioElement + audio?.pause() return audio } export const pauseOtherVideo = (idClient: string): void => { const videoElements = document.getElementsByTagName('video') - for (let i = 0; i < videoElements.length; i++) { - if (videoElements[i].id !== `msg-video-${idClient}`) { - videoElements[i].pause() + + Array.from(videoElements).forEach((item) => { + if (item.id !== `msg-video-${idClient}`) { + item.pause() } - } + }) } export const pauseAllVideo = (): void => { const videoElements = document.getElementsByTagName('video') - for (let i = 0; i < videoElements.length; i++) { - if (videoElements[i].id.startsWith('msg-video-')) { - videoElements[i].pause() + + Array.from(videoElements).forEach((item) => { + if (item.id.startsWith('msg-video-')) { + item.pause() } - } + }) } export const ParseSession: React.FC = observer( - ({ prefix = 'common', msg, replyMsg }) => { + ({ + prefix = 'common', + msg, + replyMsg, + needTextTooltop = false, + showThreadReply = false, + onReeditClick, + }) => { const _prefix = `${prefix}-parse-session` const { nim, store, localOptions } = useStateContext() const { t } = useTranslation() const locationDomRef = useRef(null) const audioContainerRef = useRef(null) + const textRef = useRef(null) const notSupportMessageText = t('notSupportMessageText') const [audioIconType, setAudioIconType] = useState('icon-yuyin3') const [imgUrl, setImgUrl] = useState('') const [replyImgUrl, setReplyImgUrl] = useState('') + const [threadReply, setThreadReply] = useState() + const [aiSearchText, setAiSearchText] = useState('') const myAccount = store.userStore.myUserInfo.accountId + const aiSearchUser = store.aiUserStore.getAISearchUser() + + let storeMsg: V2NIMMessageForUI | void = void 0 + + if (msg.threadReply) { + storeMsg = store.msgStore.getMsg(msg.threadReply.conversationId, [ + msg.threadReply.messageClientId, + ])[0] + } + + const aiErrorMap = getAIErrorMap(t) + useEffect(() => { if ( msg.messageType === @@ -84,14 +127,13 @@ export const ParseSession: React.FC = observer( msg.attachment && (msg.attachment as V2NIMMessageImageAttachment).url ) { - const url = `${ - (msg.attachment as V2NIMMessageImageAttachment).url - }?download=${(msg.attachment as V2NIMMessageImageAttachment).name}` + const url = getDownloadUrl(msg) + getBlobImg(url).then((blobUrl) => { setImgUrl(blobUrl) }) } - }, [msg.attachment, msg.messageType]) + }, [msg]) useEffect(() => { if ( @@ -101,15 +143,51 @@ export const ParseSession: React.FC = observer( replyMsg.attachment && (replyMsg.attachment as V2NIMMessageImageAttachment).url ) { - const url = `${ - (replyMsg.attachment as V2NIMMessageImageAttachment).url - }?download=${(replyMsg.attachment as V2NIMMessageImageAttachment).name}` + const url = getDownloadUrl(replyMsg) + getBlobImg(url).then((blobUrl) => { setReplyImgUrl(blobUrl) }) } }, [replyMsg]) + useEffect(() => { + if (msg.threadReply) { + if (storeMsg) { + setThreadReply(storeMsg) + } else { + nim.V2NIMMessageService.getMessageListByRefers([msg.threadReply]) + .then((res) => { + logger.log('获取 threadReply 成功', res) + if (res[0]) { + setThreadReply(store.msgStore.handleReceiveAIMsg(res[0])) + } else { + setThreadReply('noFind') + } + }) + .catch((err) => { + setThreadReply('noFind') + logger.error( + '获取 threadReply 失败', + (err as V2NIMError).toString() + ) + }) + } + } + }, [nim.V2NIMMessageService, msg.threadReply, store.msgStore, storeMsg]) + + useEffect(() => { + const handleOtherClick = () => { + setAiSearchText('') + } + + document.addEventListener('click', handleOtherClick) + + return () => { + document.removeEventListener('click', handleOtherClick) + } + }, []) + let animationFlag = false const teamId = @@ -123,18 +201,64 @@ export const ParseSession: React.FC = observer( const { EMOJI_ICON_MAP_CONFIG, INPUT_EMOJI_SYMBOL_REG } = handleEmojiTranslate(t) - const renderCustomText = (msg: V2NIMMessageForUI) => { + const handleOnMouseUp = debounce(() => { + const selection = window.getSelection() + const text = selection?.toString()?.trim() + + setAiSearchText(text || '') + }, 100) + + const handleMenuClick = async ({ key }: { key: string }) => { + if (key === AI_SEARCH_MENU_KEY && aiSearchUser) { + try { + await store.aiUserStore.sendAIProxyActive({ + accountId: aiSearchUser.accountId, + requestId: Math.random().toString(36).slice(2), + content: { msg: aiSearchText, type: 0 }, + onSendAIProxyErrorHandler: (code: number) => { + const errorText = aiErrorMap[code] || t('aiProxyFailedText') + + message.error(errorText) + }, + }) + } catch (error) { + logger.error('AI 划词搜失败', (error as V2NIMError).toString()) + } + } + } + + const getUserInfo = (account: string) => { + if ( + !account || + msg.conversationType !== + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM || + store.userStore.users.has(account) || + store.aiUserStore.aiUsers.has(account) + ) { + return + } + + store.userStore.getUserActive(account) + } + + const renderCustomText = (msg: V2NIMMessageForUI, isReplyMsg: boolean) => { const { text, messageClientId, serverExtension } = msg let finalText = reactStringReplace( text, /(https?:\/\/\S+)/gi, (match, i) => ( - + {match} ) ) + finalText = reactStringReplace( finalText, INPUT_EMOJI_SYMBOL_REG, @@ -150,11 +274,13 @@ export const ParseSession: React.FC = observer( ) if (serverExtension) { try { - const extObj = JSON.parse(serverExtension) + const extObj: YxServerExt = JSON.parse(serverExtension) const yxAitMsg = extObj.yxAitMsg + if (yxAitMsg && localOptions.needMention) { Object.keys(yxAitMsg).forEach((key) => { const item = yxAitMsg[key] + finalText = reactStringReplace( finalText, item.text, @@ -171,9 +297,53 @@ export const ParseSession: React.FC = observer( ) }) } - } catch {} + } catch { + // + } + } + + if (needTextTooltop) { + return ( + textRef.current || document.body} + > +
+ {finalText} +
+
+ ) } - return
{finalText}
+ + return ( + , + }, + ], + onClick: handleMenuClick, + }} + > +
+ {finalText} +
+
+ ) } const playAudioAnimation = () => { @@ -181,16 +351,19 @@ export const ParseSession: React.FC = observer( let audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3'] const handler = () => { const icon = audioIcons.shift() + if (icon) { setAudioIconType(icon) if (!audioIcons.length && animationFlag) { audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3'] } + if (audioIcons.length) { setTimeout(handler, 300) } } } + handler() } @@ -238,7 +411,7 @@ export const ParseSession: React.FC = observer( } const renderImage = (msg: V2NIMMessageForUI, isReplyMsg: boolean) => { - const { uploadProgress, sendingState } = msg + const { uploadProgress } = msg const attachment = msg.attachment as V2NIMMessageImageAttachment // 上传中或没有真实 url 时走这里 @@ -254,7 +427,7 @@ export const ParseSession: React.FC = observer( className={`${_prefix}-image-container`} onContextMenu={(e) => { // @ts-ignore - if (!e.target.className.includes('-image-mask')) { + if (!(e.target.className || '').includes('-image-mask')) { e.stopPropagation() } }} @@ -273,6 +446,7 @@ export const ParseSession: React.FC = observer( const renderFile = (msg: V2NIMMessageForUI) => { let downloadHref = '' + try { downloadHref = addUrlSearch( (msg.attachment as V2NIMMessageFileAttachment)?.url, @@ -298,6 +472,7 @@ export const ParseSession: React.FC = observer( download={(msg.attachment as V2NIMMessageFileAttachment)?.name} href={downloadHref} target="_blank" + rel="noreferrer" > {(msg.attachment as V2NIMMessageFileAttachment)?.name} @@ -318,21 +493,26 @@ export const ParseSession: React.FC = observer( const renderNotification = (msg: V2NIMMessageForUI) => { const attachment = msg.attachment as V2NIMMessageNotificationAttachment + switch (attachment?.type) { case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO: { const team: V2NIMTeam = (attachment?.updatedTeamInfo || {}) as V2NIMTeam const content: string[] = [] + if (team.avatar !== void 0) { content.push(t('updateTeamAvatar')) } + if (team.name !== void 0) { content.push(`${t('updateTeamName')}“${team.name}”`) } + if (team.intro !== void 0) { content.push(t('updateTeamIntro')) } + if (team.inviteMode !== void 0) { content.push( `${t('updateTeamInviteMode')}“${ @@ -345,6 +525,7 @@ export const ParseSession: React.FC = observer( }”` ) } + if (team.updateInfoMode !== void 0) { content.push( `${t('updateTeamUpdateTeamMode')}“${ @@ -358,6 +539,7 @@ export const ParseSession: React.FC = observer( }”` ) } + if (team.chatBannedMode !== void 0) { content.push( `${t('updateTeamMute')}${ @@ -369,17 +551,39 @@ export const ParseSession: React.FC = observer( }` ) } + if (team.serverExtension !== void 0) { - let ext: TAllowAt = {} + let ext: YxServerExt = {} + try { ext = JSON.parse(team.serverExtension) } catch (error) { // } - if (ext[ALLOW_AT] !== void 0) { + + if (ext.lastOpt === 'yxAllowTop' && ext.yxAllowTop !== void 0) { + content.push( + `${t('updateAllowTop')}“${ + ext.yxAllowTop === 'manager' + ? localOptions.teamManagerVisible + ? t('teamOwnerAndManagerText') + : t('onlyTeamOwner') + : t('teamAll') + }”` + ) + } else if ( + ext.lastOpt === 'yxMessageTop' && + ext.yxMessageTop !== void 0 + ) { + content.push( + ext.yxMessageTop.operation === 0 + ? t('addMessageTop') + : t('removeMessageTop') + ) + } else if (ext.yxAllowAt !== void 0) { content.push( `${t('updateAllowAt')}“${ - ext[ALLOW_AT] === 'manager' + ext.yxAllowAt === 'manager' ? localOptions.teamManagerVisible ? t('teamOwnerAndManagerText') : t('onlyTeamOwner') @@ -388,6 +592,8 @@ export const ParseSession: React.FC = observer( ) } } + + getUserInfo(msg.senderId) return content.length ? (
{store.uiStore.getAppellation({ @@ -398,12 +604,26 @@ export const ParseSession: React.FC = observer(
) : null } + // 申请加入群聊成功 case V2NIMConst.V2NIMMessageNotificationType - .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS: + .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS: { + getUserInfo(msg.senderId) + return ( +
+ {store.uiStore.getAppellation({ + account: msg.senderId, + teamId, + })}{' '} + {t('joinTeamText')} +
+ ) + } + // 邀请加入群聊对方同意 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: { + getUserInfo(msg.senderId) return (
{store.uiStore.getAppellation({ @@ -414,12 +634,14 @@ export const ParseSession: React.FC = observer(
) } + // 邀请加入群聊无需验证 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE: { const accounts: string[] = attachment?.targetIds || [] const nicks = accounts .map((item) => { + getUserInfo(item) return store.uiStore.getAppellation({ account: item, teamId, @@ -427,18 +649,21 @@ export const ParseSession: React.FC = observer( }) .filter((item) => !!item) .join('、') + return (
{nicks} {t('joinTeamText')}
) } + // 踢出群聊 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: { const accounts: string[] = attachment?.targetIds || [] const nicks = accounts .map((item) => { + getUserInfo(item) return store.uiStore.getAppellation({ account: item, teamId, @@ -446,18 +671,21 @@ export const ParseSession: React.FC = observer( }) .filter((item) => !!item) .join('、') + return (
{nicks} {t('beRemoveTeamText')}
) } + // 增加群管理员 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: { const accounts: string[] = attachment?.targetIds || [] const nicks = accounts .map((item) => { + getUserInfo(item) return store.uiStore.getAppellation({ account: item, teamId, @@ -465,18 +693,21 @@ export const ParseSession: React.FC = observer( }) .filter((item) => !!item) .join('、') + return (
{nicks} {t('beAddTeamManagersText')}
) } + // 移除群管理员 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_REMOVE_MANAGER: { const accounts: string[] = attachment?.targetIds || [] const nicks = accounts .map((item) => { + getUserInfo(item) return store.uiStore.getAppellation({ account: item, teamId, @@ -484,15 +715,18 @@ export const ParseSession: React.FC = observer( }) .filter((item) => !!item) .join('、') + return (
{nicks} {t('beRemoveTeamManagersText')}
) } + // 主动退出群聊 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: { + getUserInfo(msg.senderId) return (
{store.uiStore.getAppellation({ @@ -503,9 +737,11 @@ export const ParseSession: React.FC = observer(
) } + // 转让群主 case V2NIMConst.V2NIMMessageNotificationType .V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: { + getUserInfo((attachment?.targetIds || [])[0]) return (
@@ -518,11 +754,60 @@ export const ParseSession: React.FC = observer(
) } + default: return null } } + const renderSpecialMsg = () => { + return ( +
+ {msg.recallType === 'reCallMsg' ? ( + <> + {`${t('you')}${t('recallMessageText')}`} + {msg.canEdit ? ( + onReeditClick?.(msg)} + > + {t('reeditText')} + + ) : null} + + ) : ( + `${ + msg.isSelf + ? t('you') + : store.uiStore.getAppellation({ + account: msg.senderId, + teamId: + msg.conversationType === + V2NIMConst.V2NIMConversationType + .V2NIM_CONVERSATION_TYPE_TEAM + ? msg.receiverId + : undefined, + }) + } ${t('recallMessageText')}` + )} +
+ ) + } + + const renderTipAIMsg = (errorCode?: number) => { + const tip = aiErrorMap[errorCode || 0] + + if (!tip) { + return null + } + + return ( +
+ {tip} +
+ ) + } + const renderAudio = (msg: V2NIMMessageForUI) => { const attachment = msg.attachment as V2NIMMessageAudioAttachment const duration = Math.floor(attachment?.duration / 1000) || 0 @@ -545,12 +830,15 @@ export const ParseSession: React.FC = observer( pauseAllVideo() const oldAudio = pauseAllAudio() const msgId = oldAudio?.getAttribute('msgId') + // 如果是自己,暂停动画 if (msgId === msg.messageClientId) { animationFlag = false return } + const audio = new Audio(attachment?.url) + // 播放音频,并开始动画 audio.id = 'yx-audio-message' audio.setAttribute('msgId', msg.messageClientId) @@ -596,9 +884,7 @@ export const ParseSession: React.FC = observer( return renderUploadMsg(msg) } - url = url.startsWith('blob:') - ? url - : `${url}?download=${msg.messageClientId}.${attachment?.ext}` + url = url.startsWith('blob:') ? url : getDownloadUrl(msg) return (
) + return ( = observer( } const renderReplyMsg = () => { + if (showThreadReply && threadReply) { + return finalRenderReplyMsg(threadReply) + } + if (replyMsg) { - let content = '' - // @ts-ignore - if (replyMsg === 'noFind') { - content = t('recallReplyMessageText') - } - const nick = store.uiStore.getAppellation({ - account: replyMsg.senderId, - teamId, - ignoreAlias: true, - }) + return finalRenderReplyMsg(replyMsg) + } + + return null + } + + const finalRenderReplyMsg = (reply: V2NIMMessage | 'noFind') => { + if (reply === 'noFind') { return (
= observer( // document.getElementById(replyMsg.idClient)?.scrollIntoView() }} > - {content ? ( - content - ) : ( - <> -
{nick}:
- {renderMsgContent(replyMsg, true)} - - )} + {t('recallReplyMessageText')}
) } - return null + + const nick = store.uiStore.getAppellation({ + account: reply.senderId, + teamId, + ignoreAlias: true, + }) + + return ( +
{ + // 滚动到回复的消息 + // document.getElementById(replyMsg.idClient)?.scrollIntoView() + }} + > +
{nick}:
+ {renderMsgContent(reply, true)} +
+ ) } const renderMsgContent = (msg: V2NIMMessageForUI, isReplyMsg: boolean) => { + if (msg.recallType === 'reCallMsg' || msg.recallType === 'beReCallMsg') { + return renderSpecialMsg() + } + switch (msg.messageType) { case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT: - case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM: - return renderCustomText(msg) + getUserInfo(msg.senderId) + return renderCustomText(msg, isReplyMsg) + // case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM: case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE: + getUserInfo(msg.senderId) return renderImage(msg, isReplyMsg) case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE: + getUserInfo(msg.senderId) return renderFile(msg) case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION: return renderNotification(msg) case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO: + getUserInfo(msg.senderId) return renderAudio(msg) case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL: return `[${t('callMsgText')},${notSupportMessageText}]` case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION: + getUserInfo(msg.senderId) return renderLocation(msg) case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_ROBOT: return `[${t('robotMsgText')},${notSupportMessageText}]` case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIPS: + if ( + Object.keys(aiErrorMap) + .map((item) => Number(item)) + .includes(msg.messageStatus.errorCode) + ) { + return renderTipAIMsg(msg.messageStatus.errorCode) + } + return `[${t('tipMsgText')},${notSupportMessageText}]` case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO: + getUserInfo(msg.senderId) return renderVideo(msg) default: - return `[${t('unknowMsgText')},${notSupportMessageText}]` + return `[${notSupportMessageText}]` } } @@ -739,13 +1057,27 @@ export const ParseSession: React.FC = observer( export const getMsgContentTipByType = ( msg: Pick, t -): string => { +): string | React.ReactNode => { const { messageType, text } = msg + switch (messageType) { - case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT: - return text || `[${t('textMsgText')}]` - case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM: - return text || `[${t('customMsgText')}]` + case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT: { + const { EMOJI_ICON_MAP_CONFIG, INPUT_EMOJI_SYMBOL_REG } = + handleEmojiTranslate(t) + + return reactStringReplace(text, INPUT_EMOJI_SYMBOL_REG, (match, i) => { + return ( + + ) + }) + } + + // case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM: + // return text || `[${t('customMsgText')}]` case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO: return `[${t('audioMsgText')}]` case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE: @@ -765,6 +1097,6 @@ export const getMsgContentTipByType = ( case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO: return `[${t('videoMsgText')}]` default: - return `[${t('unknowMsgText')}]` + return `[${t('notSupportMessageText')}]` } } diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.less index 0620fea..54ee769 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.less @@ -5,6 +5,12 @@ .@{prefix}-text-wrapper { padding: 12px 16px; + .@{ant-prefix}-tooltip { + max-width: 80%; + max-height: 240px; + overflow-y: auto; + text-wrap: balance; + } } .@{prefix}-emoji-icon { @@ -53,12 +59,16 @@ .@{prefix}-audio-container { padding: 12px 16px; + display: flex; + flex-direction: column; } .@{prefix}-audio-in, .@{prefix}-audio-out { width: 50px; display: flex; justify-content: space-between; + align-items: center; + align-self: flex-end; cursor: pointer; .@{prefix}-audio-icon-wrapper { font-size: @yx-font-size-20; @@ -69,6 +79,7 @@ } .@{prefix}-audio-in { flex-direction: row-reverse; + align-self: flex-start; .@{prefix}-audio-icon-wrapper { transform: rotate(180deg); } @@ -180,3 +191,23 @@ margin: 10px 0px; } } + +.@{prefix}-recall { + padding: 10px 0; + line-height: 16px; + color: @yx-text-color-2; + text-align: center; + + .@{prefix}-reedit { + cursor: pointer; + padding-left: 6px; + color: @yx-text-color-5; + } +} + +.@{prefix}-tip { + padding: 10px 0; + line-height: 16px; + color: @yx-text-color-2; + text-align: center; +} diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.ts index c7ca3f9..391c17f 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CommonParseSession/style/index.ts @@ -1,5 +1,7 @@ import 'antd/lib/image/style' import 'antd/lib/progress/style' import 'antd/lib/popover/style' +import 'antd/lib/tooltip/style' import 'antd/lib/message/style' +import 'antd/lib/dropdown/style' import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/ComplexAvatarUI.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/ComplexAvatarUI.tsx index 8c3606d..554b00b 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/ComplexAvatarUI.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/ComplexAvatarUI.tsx @@ -36,7 +36,9 @@ export const ComplexAvatarUI: FC = ({ !onAvatarClick ? `${_prefix}-wrapper-nocursor` : '' }`} onClick={(e) => { - e.stopPropagation() + if (onAvatarClick) { + e.stopPropagation() + } }} >
diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/Container.tsx index 777d4f7..feb3d88 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/Container.tsx @@ -47,18 +47,23 @@ export const ComplexAvatarContainer: FC = observer( const { relation, isInBlacklist } = store.uiStore.getRelation(account) - const userInfo = store.uiStore.getFriendWithUserNameCard(account) + const userInfo = + relation === 'ai' + ? store.aiUserStore.aiUsers.get(account) + : store.uiStore.getFriendWithUserNameCard(account) useEffect(() => { - store.userStore.getUserActive(account) - }, [store.userStore, account]) + if (relation !== 'ai') { + store.userStore.getUserActive(account) + } + }, [store.userStore, account, relation]) useEffect(() => { - if (visible) { + if (visible && relation !== 'ai') { // 从服务端更新下个人信息 store.userStore.getUserForceActive(account) } - }, [store.uiStore, store.userStore, account, visible]) + }, [store.uiStore, store.userStore, account, visible, relation]) const handleCancel = () => { setVisible(false) @@ -80,6 +85,7 @@ export const ComplexAvatarContainer: FC = observer( }) message.success(t('addFriendSuccessText')) } + // 发送申请或添加好友成功后解除黑名单 await store.relationStore.removeUserFromBlockListActive(account) setVisible(false) @@ -149,7 +155,7 @@ export const ComplexAvatarContainer: FC = observer( const handleChangeAlias = async (alias: string) => { try { - if (userInfo.accountId) { + if (userInfo?.accountId) { await store.friendStore.setFriendInfoActive(userInfo.accountId, { alias, }) @@ -178,13 +184,13 @@ export const ComplexAvatarContainer: FC = observer( dot={dot} size={size} icon={icon} - account={userInfo.accountId} - gender={userInfo.gender as Gender} - nick={userInfo.name} - tel={userInfo.mobile} - signature={userInfo.sign} - birth={userInfo.birthday} - ext={userInfo.serverExtension} + account={userInfo?.accountId || ''} + gender={userInfo?.gender as Gender} + nick={userInfo?.name} + tel={userInfo?.mobile} + signature={userInfo?.sign} + birth={userInfo?.birthday} + ext={userInfo?.serverExtension} {...userInfo} /> ) diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/style/index.less index 5337edf..3a50dde 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/ComplexAvatar/style/index.less @@ -8,6 +8,6 @@ cursor: pointer; &-nocursor { - cursor: default; + cursor: inherit; } } diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/index.tsx new file mode 100644 index 0000000..7bcd894 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/index.tsx @@ -0,0 +1,160 @@ +import { Button, message, Modal, Input } from 'antd' +import React, { useEffect, useState } from 'react' +import { + urls, + FriendSelect, + GroupAvatarSelect, + useTranslation, + useStateContext, +} from '../../' +import { V2NIMConst } from 'nim-web-sdk-ng' +import { observer } from 'mobx-react' + +const emptyArr = [] + +export interface CreateTeamModalProps { + defaultAccounts?: string[] + visible: boolean + onChat: (teamId: string) => void + onAfterCreate?: () => void + onCancel: () => void + prefix?: string +} + +export const CreateTeamModal: React.FC = observer( + ({ + defaultAccounts = emptyArr, + visible, + onChat, + onAfterCreate, + onCancel, + prefix = 'common', + }) => { + const _prefix = `${prefix}-create-team` + + const { t } = useTranslation() + + const { store } = useStateContext() + + const [selectedList, setSelectedList] = useState(defaultAccounts) + const [groupName, setGroupName] = useState('') + const [avatarUrl, setAvatarUrl] = useState( + urls[Math.floor(Math.random() * 5)] + ) + const [teamId, setTeamId] = useState('') + const [creating, setCreating] = useState(false) + + useEffect(() => { + resetState() + }, [visible]) + + const handleCreate = async () => { + try { + setCreating(true) + const team = await store.teamStore.createTeamActive({ + accounts: selectedList, + avatar: avatarUrl, + name: groupName.trim(), + intro: '', + }) + + message.success(t('createTeamSuccessText')) + setTeamId(team.teamId) + setCreating(false) + onAfterCreate?.() + } catch (error) { + message.error(t('createTeamFailedText')) + setCreating(false) + } + } + + const handleChat = async () => { + if (teamId) { + await store.conversationStore.insertConversationActive( + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM, + teamId + ) + onChat(teamId) + } + } + + const resetState = () => { + setSelectedList(defaultAccounts) + setGroupName('') + setAvatarUrl(urls[Math.floor(Math.random() * 5)]) + setTeamId('') + setCreating(false) + } + + const footer = ( +
+ + {teamId ? ( + + ) : ( + + )} +
+ ) + + return ( + +
+
+ {t('teamTitle')} +
+ { + setGroupName(e.target.value) + }} + /> +
+
+
+ {t('teamAvatarText')} +
+ { + setAvatarUrl(url) + }} + account={''} + prefix={prefix} + /> +
+
+ { + setSelectedList(accounts) + }} + max={200} + selectedAccounts={selectedList} + prefix={prefix} + /> +
+
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.less new file mode 100644 index 0000000..e71c0b6 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.less @@ -0,0 +1,62 @@ +@import '~antd/lib/style/themes/variable.less'; +@import './theme.less'; + +.@{create-team-prefix} { + .@{ant-prefix}-modal-content { + border-radius: @yx-border-radius-8; + + .@{ant-prefix}-modal-header { + border-radius: @yx-border-radius-8 @yx-border-radius-8 0 0; + } + } + + &-group-name { + margin-top: 12px; + display: flex; + align-items: center; + + &-content { + width: 80px; + height: 16px; + text-align: center; + font-family: 'PingFang SC'; + font-style: normal; + font-weight: 400; + font-size: @yx-primary-font-size; + line-height: 16px; + color: @yx-text-color-1; + } + + &-input { + width: 260px; + height: 32px; + margin-left: 16px; + } + } + + &-group-avatar { + margin-top: 24px; + display: flex; + align-items: center; + + &-content { + width: 85px; + height: 16px; + text-align: center; + font-family: 'PingFang SC'; + font-style: normal; + font-weight: 400; + font-size: @yx-primary-font-size; + line-height: 16px; + color: @yx-text-color-1; + margin-right: 16px; + } + } + + &-group-friendList { + width: 100%; + height: 320px; + margin-top: 24px; + border-top: 1px solid @yx-border-color-7; + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.ts new file mode 100644 index 0000000..d02c898 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/index.ts @@ -0,0 +1,9 @@ +import 'antd/lib/button/style' +import 'antd/lib/message/style' +import 'antd/lib/modal/style' +import 'antd/lib/input/style' + +import '../../GroupAvatarSelect/style' +import '../../FriendSelect/style' + +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/theme.less b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/theme.less new file mode 100644 index 0000000..ef924c2 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CreateTeamModal/style/theme.less @@ -0,0 +1,3 @@ +@import '../../../style/theme.less'; + +@create-team-prefix: ~'@{common-prefix}-create-team'; diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/CrudeAvatar/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/CrudeAvatar/index.tsx index e873b00..9618c68 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/CrudeAvatar/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/CrudeAvatar/index.tsx @@ -53,19 +53,23 @@ export const CrudeAvatar: FC = ({ const store = new Storage('localStorage', '__xkit__') const key = `avatarColor-${account}` let bgColor = store.get(key) + if (!bgColor) { bgColor = colorMap[Math.floor(Math.random() * 7)] store.set(key, bgColor) } + setBgColor(bgColor) }, [account]) useEffect(() => { let webUrl = '' + if (avatar) { setImgFailed(false) webUrl = appUrlMap[avatar] } + setWebAvatar(webUrl ? webUrl : avatar) }, [avatar]) @@ -75,6 +79,7 @@ export const CrudeAvatar: FC = ({ verticalAlign: 'middle', } } + return { backgroundColor: bgColor, verticalAlign: 'middle', diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/FriendSelectItem.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/FriendSelectItem.tsx index ca0aa88..a46e5d1 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/FriendSelectItem.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/FriendSelectItem.tsx @@ -9,6 +9,7 @@ export interface FriendSelectItemProps { prefix?: string account: string appellation: string + disabled?: boolean } export const FriendSelectItem: FC = ({ @@ -18,6 +19,7 @@ export const FriendSelectItem: FC = ({ prefix = 'common', account, appellation, + disabled = false, }) => { const _prefix = `${prefix}-friend-select-item` @@ -29,6 +31,7 @@ export const FriendSelectItem: FC = ({ onChange={(e) => { onSelect?.(account, e.target.checked) }} + disabled={disabled} className={`${_prefix}-checkbox`} /> ) : null} diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/index.tsx new file mode 100644 index 0000000..623b948 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/index.tsx @@ -0,0 +1,203 @@ +import React, { FC, useMemo, useState } from 'react' +import { Divider, Spin, Tabs } from 'antd' +import { FriendSelectItem } from './FriendSelectItem' +import { groupByPy } from '../../../utils' +import { useTranslation } from '../../hooks/useTranslation' +import { useStateContext } from '../../hooks/useStateContext' +import { observer } from 'mobx-react' + +const emptyArr = [] + +export interface FriendSelectUIProps { + // 选中的人,不在 datasource 中就视为陌生人 + selectedAccounts: string[] + // 禁止勾选的人 + disabledAccounts?: string[] + onSelect: (accounts: string[]) => void + loading?: boolean + max?: number + + prefix?: string +} + +export type FriendSelectTabKey = 'friend' | 'aiUser' + +export interface DataSourceItem { + account: string + appellation: string + visible: boolean +} + +export const FriendSelect: FC = observer( + ({ + selectedAccounts, + disabledAccounts = emptyArr, + onSelect, + loading = false, + max = Infinity, + prefix = 'common', + }) => { + const _prefix = `${prefix}-friend-select` + + const { t } = useTranslation() + const { store, localOptions } = useStateContext() + + const [tab, setTab] = useState('friend') + + const dataSource = useMemo(() => { + const friendsWithoutBlacklist: DataSourceItem[] = store.uiStore.friends + .filter( + (item) => !store.relationStore.blacklist.includes(item.accountId) + ) + .map((item) => ({ + account: item.accountId, + appellation: store.uiStore.getAppellation({ + account: item.accountId, + }), + visible: tab === 'friend', + })) + + const aiUsers: DataSourceItem[] = store.aiUserStore + .getAIUserList() + .map((item) => ({ + account: item.accountId, + appellation: store.uiStore.getAppellation({ + account: item.accountId, + }), + visible: tab === 'aiUser', + })) + + const finalData = [...friendsWithoutBlacklist, ...aiUsers] + + return { + arrData: finalData, + groupByPyData: groupByPy( + finalData, + { + firstKey: 'appellation', + }, + false + ), + } + }, [store.uiStore, store.relationStore.blacklist, store.aiUserStore, tab]) + + const strangerList = useMemo(() => { + return selectedAccounts.filter((item) => + dataSource.arrData.every((j) => j.account !== item) + ) + }, [dataSource.arrData, selectedAccounts]) + + const handleSelect = (account: string, selected: boolean) => { + let _selectedAccounts: string[] = [] + + if (selected && !selectedAccounts.includes(account)) { + _selectedAccounts = selectedAccounts.concat(account) + } else if (!selected && selectedAccounts.includes(account)) { + _selectedAccounts = selectedAccounts.filter((item) => item !== account) + } + + onSelect(_selectedAccounts) + } + + const selectedList = useMemo(() => { + return dataSource.arrData.filter((item) => + selectedAccounts.includes(item.account) + ) + }, [dataSource.arrData, selectedAccounts]) + + const renderTab = () => { + const items: { + key: FriendSelectTabKey + label: string + }[] = [ + { + key: 'friend', + label: t('friendText'), + }, + { + key: 'aiUser', + label: t('aiUserText'), + }, + ] + + return localOptions.aiVisible ? ( + { + setTab(key as FriendSelectTabKey) + }} + /> + ) : null + } + + return ( +
+ {loading ? ( + + ) : ( + <> +
+ {renderTab()} +
+ {dataSource.groupByPyData.map(({ key, data }) => { + if (data.every((item) => !item.visible)) { + return null + } + + return ( +
+
{key}
+ {data + .filter((item) => item.visible) + .map((item) => ( + = max && + !selectedAccounts.includes(item.account)) + } + prefix={prefix} + {...item} + /> + ))} +
+ ) + })} +
+
+ +
+
+ {t('selectedText')}:{selectedList.length} {t('personUnit')} + {strangerList.length ? ( + <> + ,{strangerList.length} {t('strangerText')} + + ) : null} +
+
+ {selectedList.map((item) => ( + + ))} +
+
+ + )} +
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.less index 9f6bf5c..5e77ec9 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.less @@ -10,13 +10,18 @@ display: flex; flex-direction: row; align-items: center; - padding: 10px; } &-left { height: 100%; - overflow-y: auto; + display: flex; + flex-direction: column; + flex: 1; + } + + &-list { flex: 1; + overflow-y: auto; } &-right { @@ -39,7 +44,7 @@ &-selected-title { font-size: @yx-primary-font-size; color: @yx-text-color-3; - height: 22px; + padding: 12px; } &-selected-content { @@ -50,7 +55,7 @@ &-item { display: flex; align-items: center; - margin-top: 8px; + padding: 8px 0; &-checkbox.@{ant-prefix}-checkbox-wrapper { margin-right: 8px; @@ -62,4 +67,16 @@ font-size: @yx-font-size-12; } } + + &-tabs { + .@{ant-prefix}-tabs-nav-list { + width: 100%; + justify-content: space-between; + } + + .@{ant-prefix}-tabs-tab { + margin: 0; + padding: 12px 35px; + } + } } diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.ts index 951f8d0..098590f 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/FriendSelect/style/index.ts @@ -1,7 +1,7 @@ -import '../../CrudeAvatar/style' - -import './index.less' - import 'antd/lib/checkbox/style' import 'antd/lib/divider/style' -import 'antd/lib/message/style' +import 'antd/lib/tabs/style' + +import '../../ComplexAvatar/style' + +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/GroupAvatarSelect/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/GroupAvatarSelect/index.tsx index d21700a..e10c8fb 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/GroupAvatarSelect/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/GroupAvatarSelect/index.tsx @@ -1,6 +1,7 @@ import React, { FC, useMemo, useState, useEffect } from 'react' import { Tooltip, Button } from 'antd' import { CrudeAvatar, CrudeAvatarProps } from '../CrudeAvatar' + export interface GroupAvatarSelectProps extends CrudeAvatarProps { onSelect: (url: string) => void prefix?: string diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/MyAvatar/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/MyAvatar/Container.tsx index 37c5a4d..6a364d6 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/MyAvatar/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/MyAvatar/Container.tsx @@ -57,21 +57,27 @@ export const MyAvatarContainer: FC = observer( signature?: string }) => { const params: V2NIMUserUpdateParams = {} + if (gender !== void 0) { params.gender = gender } + if (email !== void 0) { params.email = email } + if (nick !== void 0) { params.name = nick } + if (tel !== void 0) { params.mobile = tel } + if (signature !== void 0) { params.sign = signature } + store.userStore .updateSelfUserProfileActive(params, avatarFile) .then(() => { diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/MyUserCard/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/MyUserCard/index.tsx index 0faaa12..98bcc8b 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/MyUserCard/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/MyUserCard/index.tsx @@ -109,7 +109,7 @@ export const MyUserCard: FC = ({ onCancel?.() } - const handleSave = (e: React.MouseEvent) => { + const handleSave = () => { if ( email && !email.includes('@') && @@ -119,6 +119,7 @@ export const MyUserCard: FC = ({ message.error(t('emailErrorText')) return } + onSave?.({ avatar, avatarFile, @@ -130,17 +131,20 @@ export const MyUserCard: FC = ({ }) } - const handleBeforeUpload = (file: any, FileList: any[]) => { + const handleBeforeUpload = (file: any) => { const LIMIT = 5 const isLt5M = file.size / 1024 / 1024 < LIMIT + if (!isLt5M) { message.error(`${t('uploadLimitText')}${LIMIT}${t('uploadLimitUnit')}`) } + return isLt5M } const handleUpload = (file: any): any => { const reader = new FileReader() + reader.readAsDataURL(file) reader.onload = () => { setAvatar(reader.result as string) diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/index.tsx new file mode 100644 index 0000000..9b736ed --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/index.tsx @@ -0,0 +1,53 @@ +import React, { FC } from 'react' + +export interface RichTextProps { + ref?: React.RefObject + className?: string + placeholder?: string + children?: React.ReactNode + disabled?: boolean + onInput?: React.FormEventHandler + onKeyDown?: React.KeyboardEventHandler + onPressEnter?: (event: React.KeyboardEvent) => void + onClick?: React.MouseEventHandler + + prefix?: string +} + +export const RichText: FC = ({ + ref, + className = '', + placeholder, + children, + disabled, + onInput, + onKeyDown, + onPressEnter, + onClick, + prefix = 'common', +}) => { + const _prefix = `${prefix}-rich-text` + + const handleOnKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + onPressEnter?.(event) + return + } + + onKeyDown?.(event) + } + + return ( +
+ {children} +
+ ) +} diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.less new file mode 100644 index 0000000..96790fc --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.less @@ -0,0 +1,4 @@ +@import './theme.less'; + +.@{richtext-prefix} { +} diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.ts new file mode 100644 index 0000000..1526e4d --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/index.ts @@ -0,0 +1 @@ +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/theme.less b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/theme.less new file mode 100644 index 0000000..393c294 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/RichText/style/theme.less @@ -0,0 +1,3 @@ +@import '../../../style/theme.less'; + +@richtext-prefix: ~'@{common-prefix}-richtext'; diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/index.tsx index 177424f..740b0bc 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/index.tsx @@ -7,9 +7,11 @@ export interface SelectModalItemProps { key: string label: string disabled?: boolean + hide?: boolean } export interface SelectModalProps { + tabRenderer?: React.ReactNode datasource: SelectModalItemProps[] visible: boolean onSearchChange?: (value: string) => void @@ -22,11 +24,13 @@ export interface SelectModalProps { defaultValue?: string[] bottomRenderer?: React.ReactNode itemAvatarRender?: (data: SelectModalItemProps) => React.ReactNode + recentRenderer?: React.ReactNode type?: 'radio' | 'checkbox' max?: number min?: number searchPlaceholder?: string leftTitle?: string + showLeftTitle?: boolean rightTitle?: string closable?: boolean width?: number @@ -37,6 +41,7 @@ export interface SelectModalProps { const emptyArr = [] export const SelectModal: React.FC = ({ + tabRenderer, datasource, visible, onSearchChange, @@ -49,11 +54,13 @@ export const SelectModal: React.FC = ({ defaultValue = emptyArr, bottomRenderer, itemAvatarRender, + recentRenderer, type = 'radio', max = Infinity, min = 0, searchPlaceholder, leftTitle, + showLeftTitle = true, rightTitle, closable = true, width = 720, @@ -92,22 +99,33 @@ export const SelectModal: React.FC = ({ const _prefix = `${prefix}-select-modal` const getItemsFromKeys = (keys: string[]) => { - return datasource.filter((item) => keys.some((j) => j === item.key)) + return datasource + .filter((item) => keys.some((j) => j === item.key)) + .reduce((unique, item) => { + if (!unique.some((uniqueItem) => uniqueItem.key === item.key)) { + unique.push(item) + } + + return unique + }, [] as SelectModalItemProps[]) } const handleSearchTextChange = (e: any) => { const value = e.target.value + setSearchText(value) onSearchChange?.(value) } const handleSelect = (e: any) => { let value: string[] = [] + if (type === 'radio') { value = [e.target.value] } else { value = e } + setSelected(value) onSelectChange?.(getItemsFromKeys(value)) } @@ -141,14 +159,15 @@ export const SelectModal: React.FC = ({ value={selected[0]} style={{ width: '100%' }} > - {datasource.map((item) => { - const isVisible = item.label.includes(searchText) + {datasource.map((item, index) => { + const isVisible = !item.hide && item.label.includes(searchText) + return (
@@ -169,14 +188,15 @@ export const SelectModal: React.FC = ({ style={{ width: '100%' }} disabled={selected.length >= max} > - {datasource.map((item) => { - const isVisible = item.label.includes(searchText) + {datasource.map((item, index) => { + const isVisible = !item.hide && item.label.includes(searchText) + return (
@@ -193,9 +213,11 @@ export const SelectModal: React.FC = ({ return selected.length ? selected.map((key) => { const item = datasource.find((item) => item.key === key) + if (!item) { return null } + return (
{itemAvatarRender?.(item)} @@ -235,12 +257,17 @@ export const SelectModal: React.FC = ({ onChange={handleSearchTextChange} placeholder={searchPlaceholder} /> -
- {searchText ? t('searchText') : leftTitle} -
+ {!searchText ? recentRenderer : null} + {showLeftTitle ? ( +
+ {searchText ? t('searchText') : leftTitle} +
+ ) : null} + {tabRenderer}
- {!datasource.filter((item) => item.label.includes(searchText)) - .length ? ( + {!datasource.filter( + (item) => !item.hide && item.label.includes(searchText) + ).length ? (
{t('searchNoResText')}
diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/style/index.less b/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/style/index.less index 9c1a0d9..0867f1b 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/style/index.less +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/SelectModal/style/index.less @@ -7,6 +7,8 @@ border-radius: @yx-primary-border-radius; display: flex; flex-direction: row; + min-height: 400px; + max-height: 540px; .@{ant-prefix}-checkbox-wrapper { margin-right: 8px; @@ -14,15 +16,17 @@ &-left { flex: 1; + display: flex; + flex-direction: column; padding: 12px; border-right: 1px solid @yx-border-color-10; } &-right { flex: 1; + display: flex; + flex-direction: column; padding: 8px; - max-height: 388px; - overflow-y: auto; } &-l-title { @@ -39,12 +43,12 @@ } &-l-list { - height: 300px; + flex: 1; overflow-y: auto; } &-r-list { - max-height: 350px; + flex: 1; overflow-y: auto; } diff --git a/react/src/YXUIKit/im-kit-ui/src/common/components/UserCard/index.tsx b/react/src/YXUIKit/im-kit-ui/src/common/components/UserCard/index.tsx index f2778e7..ab1109d 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/components/UserCard/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/components/UserCard/index.tsx @@ -144,13 +144,15 @@ export const UserCard: FC = ({ {props.alias || props.nick || props.account} - - - + {relation !== 'ai' ? ( + + + + ) : null}
{relation === 'friend' ? ( @@ -171,30 +173,38 @@ export const UserCard: FC = ({ {props.account}
-
- - - { - ( - genderOptions.find((item) => item.value === props.gender) || { - label: t('unknow'), - } - ).label - } - -
-
- - - {props.tel || ''} - -
-
- - - {props.email || ''} - -
+ {relation !== 'ai' ? ( +
+ + + { + ( + genderOptions.find( + (item) => item.value === props.gender + ) || { + label: t('unknow'), + } + ).label + } + +
+ ) : null} + {relation !== 'ai' ? ( +
+ + + {props.tel || ''} + +
+ ) : null} + {relation !== 'ai' ? ( +
+ + + {props.email || ''} + +
+ ) : null}
diff --git a/react/src/YXUIKit/im-kit-ui/src/common/contextManager/Provider.tsx b/react/src/YXUIKit/im-kit-ui/src/common/contextManager/Provider.tsx index b41106d..731c27c 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/contextManager/Provider.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/common/contextManager/Provider.tsx @@ -11,10 +11,9 @@ import RootStore from '@xkit-yx/im-store-v2' import { LocalOptions } from '@xkit-yx/im-store-v2/dist/types/types' import { observer } from 'mobx-react' import { useStateContext } from '../hooks/useStateContext' -import V2NIM from 'nim-web-sdk-ng' +import V2NIM, { V2NIMConst } from 'nim-web-sdk-ng' import zh from '../locales/zh' import sdkPkg from 'nim-web-sdk-ng/package.json' -import { V2NIMConst } from 'nim-web-sdk-ng' export interface ContextProps { nim?: V2NIM @@ -46,7 +45,7 @@ const defaultLocalOptions: Required = { V2NIMConst.V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_MANAGER, teamUpdateExtMode: V2NIMConst.V2NIMTeamUpdateExtensionMode - .V2NIM_TEAM_UPDATE_EXTENSION_MODE_MANAGER, + .V2NIM_TEAM_UPDATE_EXTENSION_MODE_ALL, leaveOnTransfer: false, needMention: true, p2pMsgReceiptVisible: false, @@ -54,8 +53,11 @@ const defaultLocalOptions: Required = { loginStateVisible: false, allowTransferTeamOwner: false, teamManagerVisible: false, + aiVisible: true, teamManagerLimit: 10, sendMsgBefore: async (options: any) => options, + aiUserAgentProvider: {}, + conversationLimit: 100, } export const Provider: FC = memo( @@ -94,6 +96,7 @@ export const Provider: FC = memo( if (singleton) { return RootStore.getInstance(nim, finalLocalOptions) } + return new RootStore(nim, finalLocalOptions) }, [nim, singleton, finalLocalOptions]) diff --git a/react/src/YXUIKit/im-kit-ui/src/common/hooks/useEventTracking.ts b/react/src/YXUIKit/im-kit-ui/src/common/hooks/useEventTracking.ts index db33e0d..9a834bd 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/hooks/useEventTracking.ts +++ b/react/src/YXUIKit/im-kit-ui/src/common/hooks/useEventTracking.ts @@ -21,6 +21,7 @@ export const useEventTracking = ({ component: component, imVersion: imVersion, }) + eventTracking.track('init', '') }, [appkey, version, component, imVersion]) } diff --git a/react/src/YXUIKit/im-kit-ui/src/common/index.ts b/react/src/YXUIKit/im-kit-ui/src/common/index.ts index ec813e4..39150f5 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/common/index.ts @@ -13,10 +13,12 @@ export type { ProviderProps, ContextProps } from './contextManager/Provider' export { UserCard } from './components/UserCard' export { MyUserCard } from './components/MyUserCard' export { GroupAvatarSelect, urls } from './components/GroupAvatarSelect' -export { FriendSelectContainer } from './components/FriendSelect/Container' +export { FriendSelect } from './components/FriendSelect' +export { CreateTeamModal } from './components/CreateTeamModal' export { Welcome } from './components/Welcome' export { ReadPercent } from './components/ReadPercent' export { SelectModal } from './components/SelectModal' +export { RichText } from './components/RichText' export { ParseSession, getMsgContentTipByType, diff --git a/react/src/YXUIKit/im-kit-ui/src/common/locales/zh.ts b/react/src/YXUIKit/im-kit-ui/src/common/locales/zh.ts index 899823a..99eb1e4 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/locales/zh.ts +++ b/react/src/YXUIKit/im-kit-ui/src/common/locales/zh.ts @@ -13,7 +13,9 @@ const LocaleConfig = { deleteText: '删除', recallText: '撤回', forwardText: '转发', + forwardSuccessText: '转发成功', forwardFailedText: '转发失败', + recentForwardText: '最近转发', sendBtnText: '发送', replyText: '回复', commentText: '留言', @@ -32,7 +34,6 @@ const LocaleConfig = { removeBlackFailedText: '解除拉黑失败', maxSelectedText: '最多只能选择', selectedText: '已选', - friendsText: '位好友', strangerText: '位陌生人', emailErrorText: '邮箱格式不正确', uploadLimitText: '图片视频或文件大小最大支持', @@ -77,6 +78,9 @@ const LocaleConfig = { updateTeamInviteMode: '更新了群权限“邀请他人权限”为', updateTeamUpdateTeamMode: '更新了群权限“群资料修改权限”为', updateAllowAt: '更新了“@所有人权限”为', + updateAllowTop: '置顶消息权限更新为', + addMessageTop: '置顶了一条消息', + removeMessageTop: '移除了置顶消息', updateTeamMute: '更新了“群禁言”为', onlyTeamOwner: '仅群主', teamAll: '所有人', @@ -110,17 +114,22 @@ const LocaleConfig = { fileMsgText: '文件消息', callMsgText: '话单消息', geoMsgText: '地理位置消息', + geoMsgShortText: '位置', imgMsgText: '图片消息', notiMsgText: '通知消息', robotMsgText: '机器消息', tipMsgText: '提示消息', unknowMsgText: '未知消息', deleteSessionText: '删除会话', + recentConversationText: '最近会话', muteSessionText: '开启免打扰', unmuteSessionText: '取消免打扰', deleteStickTopText: '取消置顶', addStickTopText: '置顶消息', beMentioned: '[有人@我]', + aiConversationSelectFailed: '选择 AI 会话失败', + friendText: '好友', + aiUserText: '数字人', // contact-kit teamListTitle: '我的群组', @@ -128,9 +137,11 @@ const LocaleConfig = { blackListTitle: '黑名单', msgListTitle: '消息中心', blackListDesc: '(你不会收到列表中任何联系人的消息)', + aiListTitle: '我的数字人', teamMenuText: '我的群组', friendMenuText: '我的好友', blackMenuText: '黑名单', + aiMenuText: '我的数字人', msgMenuText: '消息中心', acceptedText: '已同意该申请', acceptFailedText: '同意该申请失败', @@ -143,6 +154,7 @@ const LocaleConfig = { createTeamText: '创建群组', joinTeamText: '加入群组', joinTeamSuccessText: '加入群组成功', + joinTeamFailedText: '加入群组失败', beRemoveTeamText: '被移出群组', addButtonText: '添加', addSuccessText: '添加成功', @@ -167,6 +179,16 @@ const LocaleConfig = { // chat-kit sendToText: '发送给', + topText: '置顶', + unTopText: '取消置顶', + topFailedText: '置顶失败', + collection: '收藏', + collectionSuccess: '已收藏', + collectionFailed: '收藏失败', + getCollectionFailed: '查询收藏列表失败', + removeCollectionSuccess: '删除收藏成功', + removeCollectionFailed: '删除收藏失败', + confirmRemoveCollection: '确认删除收藏?', sendUsageText: '(按enter直接发送,shift+enter换行)', sendEmptyText: '不能发送空白消息', teamMutePlaceholder: '当前群聊禁言中', @@ -208,6 +230,7 @@ const LocaleConfig = { teamManagerLimitText: '谁可以修改资料', teamInviteModeText: '谁可以邀请新成员', teamAtModeText: '谁可以@所有人', + teamTopModeText: '谁可以置顶消息', teamMemberText: '群成员', teamInfoText: '群资料', teamPowerText: '群管理', @@ -231,10 +254,27 @@ const LocaleConfig = { sendNotFriendFailedText: '当前非好友关系', recallMessageText: '撤回了一条消息', recallReplyMessageText: '该消息已撤回或删除', + tipAIMessageText: '格式不支持', + tipAIFailedMessageText: '请求大语言模型失败', + memberNotExistsText: '用户不存在', + aiAntiSpamText: 'AI 请求命中反垃圾', + aiFunctionDisabled: 'AI 消息功能未开通', + aiMemberBanned: '用户被禁用', + aiMemberChatBanned: '用户被禁言', + aiFriendNotExists: '好友不存在', + aiMessageHitAntiSpam: '消息命中反垃圾', + notAnAi: '不是数字人账号', + aiTeamMemberNotExists: '群成员不存在', + aiNormalTeamChatBanned: '群普通成员禁言', + aiTeamChatBanned: '群成员被禁言', + aiBlockFailedText: '不允许对数字人进行黑名单操作', + aiRateLimit: '频率超限', + aiParameterError: '参数错误', reeditText: '重新编辑', addChatMemberText: '添加聊天成员', chatHistoryText: '聊天记录', noMoreText: '没有更多消息了', + noMoreCollectionText: '已滑到最底部~', receiveText: '您收到了新消息', strangerNotiText: '当前不是您的好友,请注意保护个人隐私安全。', nickInTeamText: '我在群里的昵称', @@ -247,6 +287,18 @@ const LocaleConfig = { videoText: '视频', onlineText: '[在线]', offlineText: '(离线)', + pinAIText: 'PIN 置顶', + aiSearchText: 'AI 划词搜', + aiSearchingText: 'AI 划词搜索中...', + aiProxyFailedText: '模型请求异常', + aiSearchInputPlaceholder: '补充输入更多信息', + aiTranslateText: 'AI 处理', + aiTranslatingText: 'AI 处理中...', + aiTranslatedText: '使用', + aiTranslatePlaceholder: '翻译为...', + aiTranslateEmptyText: '请输入需要翻译的内容', + aiSendingText: '大模型请求响应中', + searchTipText: 'Enter 搜索', // emoji 不能随便填,要用固定 key,,参考 demo Laugh: '[大笑]', @@ -275,6 +327,7 @@ const LocaleConfig = { Halo: '[两眼冒星]', Shame: '[害羞]', Sleep: '[睡着]', + Sleeping: '[睡觉]', Tired: '[冒星]', Mask: '[口罩]', ok: '[OK]', @@ -285,7 +338,8 @@ const LocaleConfig = { ill: '[不舒服]', Mad: '[愤怒]', Ghost: '[鬼怪]', - Angry: '[发怒]', + huff: '[发怒]', + Angry: '[生气]', Unhappy: '[不高兴]', Frown: '[皱眉]', Broken: '[心碎]', diff --git a/react/src/YXUIKit/im-kit-ui/src/common/themes/variables.less b/react/src/YXUIKit/im-kit-ui/src/common/themes/variables.less index 6822fe2..3b59a1c 100644 --- a/react/src/YXUIKit/im-kit-ui/src/common/themes/variables.less +++ b/react/src/YXUIKit/im-kit-ui/src/common/themes/variables.less @@ -44,6 +44,7 @@ @yx-background-color-5: #d7e4ff; @yx-background-color-6: #f1f5f8; @yx-background-color-7: #f1f4f8; +@yx-background-color-8: #e9eaec; // >>> Icon @yx-icon-color-1: #656a72; diff --git a/react/src/YXUIKit/im-kit-ui/src/constant.ts b/react/src/YXUIKit/im-kit-ui/src/constant.ts index 768f769..828fa1a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/constant.ts +++ b/react/src/YXUIKit/im-kit-ui/src/constant.ts @@ -5,7 +5,3 @@ export const SHOW_RECALL_BTN_MSG_TIME = 1000 * 60 * 2 export const MIN_VALUE = Number.MIN_VALUE export const MAX_UPLOAD_FILE_SIZE = 100 - -export const ALLOW_AT = 'yxAllowAt' - -export type TAllowAt = { yxAllowAt?: 'manager' | 'all' } diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/Container.tsx new file mode 100644 index 0000000..af6b077 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/Container.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react' +import { AIList } from './components/AIList' +import { useEventTracking, useStateContext } from '../../common' +import packageJson from '../../../package.json' +import { observer } from 'mobx-react' +import sdkPkg from 'nim-web-sdk-ng/package.json' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' + +export interface AIListContainerProps { + /** + 数字人点击事件 + */ + onItemClick?: (aiUser: V2NIMAIUser) => void + /** + 点击发送消息后的事件 + */ + afterSendMsgClick?: () => void + /** + 自定义渲染数字人列表为空时内容 + */ + renderAIListEmpty?: () => JSX.Element + /** + 自定义渲染数字人列表头部内容 + */ + renderAIListHeader?: () => JSX.Element + /** + 样式前缀 + */ + prefix?: string + /** + 公共样式前缀 + */ + commonPrefix?: string +} + +export const AIListContainer: FC = observer( + ({ + onItemClick, + afterSendMsgClick, + renderAIListEmpty, + renderAIListHeader, + prefix = 'contact', + commonPrefix = 'common', + }) => { + const { store, nim } = useStateContext() + + const aiUsers = store.aiUserStore.getAIUserList() + + useEventTracking({ + appkey: nim.options.appkey, + version: packageJson.version, + component: 'ContactUIKit', + imVersion: sdkPkg.version, + }) + + return ( + + ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIItem.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIItem.tsx new file mode 100644 index 0000000..d57a98e --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIItem.tsx @@ -0,0 +1,47 @@ +import React, { FC } from 'react' +import { ComplexAvatarContainer, useStateContext } from '../../../common' +import { observer } from 'mobx-react' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' + +export interface AIItemProps { + aiUser: V2NIMAIUser + onItemClick?: (aiUser: V2NIMAIUser) => void + afterSendMsgClick?: () => void + prefix?: string + commonPrefix?: string +} + +export const AIItem: FC = observer( + ({ + aiUser, + onItemClick, + afterSendMsgClick, + prefix = 'contact', + commonPrefix = 'common', + }) => { + const _prefix = `${prefix}-ai-item` + + const { store } = useStateContext() + + return ( +
{ + e.stopPropagation() + onItemClick?.(aiUser) + }} + > + + + {store.uiStore.getAppellation({ + account: aiUser.accountId, + })} + +
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIList.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIList.tsx new file mode 100644 index 0000000..1a3d571 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/components/AIList.tsx @@ -0,0 +1,61 @@ +import React, { FC } from 'react' +import { AIItem } from './AIItem' +import { useTranslation } from '../../../common' +import { Spin, Empty } from 'antd' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' + +export interface AIListProps { + list: V2NIMAIUser[] + loading?: boolean + onItemClick?: (aiUser: V2NIMAIUser) => void + afterSendMsgClick?: () => void + renderAIListHeader?: () => JSX.Element + renderAIListEmpty?: () => JSX.Element + prefix?: string + commonPrefix?: string +} + +export const AIList: FC = ({ + list, + loading = false, + onItemClick, + afterSendMsgClick, + renderAIListEmpty, + renderAIListHeader, + prefix = 'contact', + commonPrefix = 'common', +}) => { + const _prefix = `${prefix}-ai` + + const { t } = useTranslation() + + return ( +
+
+ {renderAIListHeader ? renderAIListHeader() : t('aiListTitle')} +
+
+ {loading ? ( + + ) : !list.length ? ( + renderAIListEmpty ? ( + renderAIListEmpty() + ) : ( + + ) + ) : ( + list.map((item) => ( + + )) + )} +
+
+ ) +} diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.less b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.less new file mode 100644 index 0000000..e374c27 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.less @@ -0,0 +1,49 @@ +@import './theme.less'; + +@ai-list-prefix-cls: ~'@{ai-list-prefix}-wrapper'; +@ai-title-prefix-cls: ~'@{ai-list-prefix}-title'; +@ai-content-prefix-cls: ~'@{ai-list-prefix}-content'; +@ai-item-prefix-cls: ~'@{ai-list-prefix}-item'; + +.@{ai-list-prefix-cls} { + background-color: @yx-background-color-1; + width: 100%; + height: 100%; + overflow-y: auto; +} + +.@{ai-title-prefix-cls} { + color: @yx-primary-text-color; + font-size: @yx-font-size-16; + font-weight: 500; + border-bottom: 1px solid @yx-border-color-7; + padding: 22px 16px; +} + +.@{ai-content-prefix-cls} { + padding: 0 16px; +} + +.@{ai-item-prefix-cls} { + cursor: pointer; + padding: 16px 0px; + display: flex; + align-items: center; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + width: 100%; + border-radius: 4px; + + &:not(:last-child) { + border-bottom: 1px solid @yx-border-color-7; + } + + &:hover { + background-color: @yx-background-color-4; + } + + &-label { + margin-left: 12px; + font-size: @yx-primary-font-size; + color: @yx-primary-text-color; + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.ts new file mode 100644 index 0000000..7b09c61 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/index.ts @@ -0,0 +1,6 @@ +import 'antd/lib/spin/style' +import 'antd/lib/empty/style' + +import '../../../common/components/ComplexAvatar/style' + +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/theme.less b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/theme.less new file mode 100644 index 0000000..bbd64c1 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/contact/ai-list/style/theme.less @@ -0,0 +1,3 @@ +@import '../../style/theme.less'; + +@ai-list-prefix: ~'@{contact-kit-prefix}-ai'; diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/Container.tsx index d26ef1d..600d25a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/Container.tsx @@ -5,6 +5,7 @@ import { FriendListContainer } from '../friend-list/Container' import { BlackListContainer } from '../black-list/Container' import { GroupListContainer } from '../group-list/Container' import { MsgListContainer } from '../msg-list/Container' +import { AIListContainer } from '../ai-list/Container' import packageJson from '../../../package.json' import { V2NIMTeam } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' @@ -13,6 +14,7 @@ import { V2NIMFriendAddApplicationForUI, V2NIMTeamJoinActionInfoForUI, } from '@xkit-yx/im-store-v2/dist/types/types' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' export interface ContactInfoContainerProps { /** @@ -51,6 +53,10 @@ export interface ContactInfoContainerProps { 黑名单点击事件 */ onBlackItemClick?: (account: string) => void + /** + * AI 数字人点击事件 + */ + onAIItemClick?: (aiUser: V2NIMAIUser) => void /** 群组点击事件 */ @@ -71,6 +77,14 @@ export interface ContactInfoContainerProps { 自定义渲染黑名单列表头部内容 */ renderBlackListHeader?: () => JSX.Element + /** + 自定义渲染数字人列表为空时内容 + */ + renderAIListEmpty?: () => JSX.Element + /** + 自定义渲染数字人列表头部内容 + */ + renderAIListHeader?: () => JSX.Element /** 自定义渲染群组列表为空时内容 */ @@ -105,10 +119,13 @@ export const ContactInfoContainer: React.FC = observer( ({ onBlackItemClick, + onAIItemClick, onFriendItemClick, onGroupItemClick, renderBlackListEmpty, renderBlackListHeader, + renderAIListEmpty, + renderAIListHeader, renderFriendListEmpty, renderFriendListHeader, renderGroupListEmpty, @@ -174,6 +191,15 @@ export const ContactInfoContainer: React.FC = prefix={prefix} commonPrefix={commonPrefix} /> + ) : store.uiStore.selectedContactType === 'aiList' ? ( + ) : renderEmpty ? ( renderEmpty() ) : ( diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/style/index.ts index 6b25798..634f81a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/contact/contact-info/style/index.ts @@ -2,3 +2,4 @@ import '../../black-list/style' import '../../friend-list/style' import '../../group-list/style' import '../../msg-list/style' +import '../../ai-list/style' diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/Container.tsx index 4588919..1005ac8 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/Container.tsx @@ -41,6 +41,7 @@ export const ContactListContainer: FC = observer( if (contactType === 'msgList') { store.sysMsgStore.setAllApplyMsgRead() } + onItemClick?.(contactType) } diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactItem.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactItem.tsx index ba86f50..9b02c67 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactItem.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactItem.tsx @@ -5,6 +5,7 @@ import { ContactType } from '@xkit-yx/im-store-v2' export interface ContactItemProps { icon: ReactNode label: string + show: boolean contactType: ContactType isSelectd?: boolean backgroundColor: string diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactList.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactList.tsx index de328b3..25594bf 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactList.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/contact/contact-list/components/ContactList.tsx @@ -1,8 +1,9 @@ import React, { FC } from 'react' -import { UserOutlined, TeamOutlined } from '@ant-design/icons' +import { UserOutlined, TeamOutlined, RobotOutlined } from '@ant-design/icons' import { ContactItemProps, ContactItem } from './ContactItem' -import { useTranslation } from '../../../common' +import { CommonIcon, useStateContext, useTranslation } from '../../../common' import { ContactType } from '@xkit-yx/im-store-v2' +import { observer } from 'mobx-react' export interface ContactListProps { selectedContactType: ContactType | '' @@ -14,71 +15,91 @@ export interface ContactListProps { prefix?: string } -export const ContactList: FC = ({ - selectedContactType, - onItemClick, - renderCustomContact, - systemMsgUnread, - prefix = 'contact', -}) => { - const _prefix = `${prefix}-list` +export const ContactList: FC = observer( + ({ + selectedContactType, + onItemClick, + renderCustomContact, + systemMsgUnread, + prefix = 'contact', + }) => { + const _prefix = `${prefix}-list` - const { t } = useTranslation() + const { t } = useTranslation() - const dataSource: ContactItemProps[] = [ - { - contactType: 'msgList', - label: t('msgMenuText'), - icon: , - backgroundColor: '#60CFA7', - onItemClick: (contactType) => { - onItemClick(contactType) + const { localOptions } = useStateContext() + + const dataSource: ContactItemProps[] = [ + { + contactType: 'msgList', + label: t('msgMenuText'), + show: true, + icon: , + backgroundColor: '#60CFA7', + onItemClick: (contactType) => { + onItemClick(contactType) + }, + unread: systemMsgUnread, + }, + { + contactType: 'blackList', + label: t('blackMenuText'), + show: true, + icon: , + backgroundColor: '#53C3F3', + onItemClick: (contactType) => { + onItemClick(contactType) + }, }, - unread: systemMsgUnread, - }, - { - contactType: 'blackList', - label: t('blackMenuText'), - icon: , - backgroundColor: '#53C3F3', - onItemClick: (contactType) => { - onItemClick(contactType) + { + contactType: 'friendList', + label: t('friendMenuText'), + show: true, + icon: , + backgroundColor: '#537FF4', + onItemClick: (contactType) => { + onItemClick(contactType) + }, }, - }, - { - contactType: 'friendList', - label: t('friendMenuText'), - icon: , - backgroundColor: '#537FF4', - onItemClick: (contactType) => { - onItemClick(contactType) + { + contactType: 'groupList', + label: t('teamMenuText'), + show: true, + icon: , + backgroundColor: '#BE65D9', + onItemClick: (contactType) => { + onItemClick(contactType) + }, }, - }, - { - contactType: 'groupList', - label: t('teamMenuText'), - icon: , - backgroundColor: '#BE65D9', - onItemClick: (contactType) => { - onItemClick(contactType) + { + contactType: 'aiList', + label: t('aiMenuText'), + show: !!localOptions.aiVisible, + icon: , + backgroundColor: '#854FE2', + onItemClick: (contactType) => { + onItemClick(contactType) + }, }, - }, - ] + ] - return ( -
- {dataSource.map((item) => { - return ( - renderCustomContact?.(item.contactType) ?? ( - - ) - ) - })} -
- ) -} + return ( +
+ {dataSource + .filter((item) => item.show) + .map((item) => { + return ( + renderCustomContact?.(item.contactType) ?? ( + + ) + ) + })} +
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/contact/friend-list/components/FriendList.tsx b/react/src/YXUIKit/im-kit-ui/src/contact/friend-list/components/FriendList.tsx index cac6eaf..7959c3a 100644 --- a/react/src/YXUIKit/im-kit-ui/src/contact/friend-list/components/FriendList.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/contact/friend-list/components/FriendList.tsx @@ -45,10 +45,12 @@ export const FriendList: FC = ({ ) const res: ({ account: string; appellation: string } | string)[] = [] + group.forEach((item) => { if (!res.includes(item.key)) { res.push(item.key) } + res.push(...item.data) }) return res @@ -79,6 +81,7 @@ export const FriendList: FC = ({
) } + return ( = ({ ...props }) => { const _prefix = `${prefix}-group-item` + return (
= observer( ({ msg, - applyTeamLoaidng = false, - teamInviteLoading = false, applyFriendLoading = false, - onAcceptApplyTeamClick, - onRejectApplyTeamClick, - onAcceptTeamInviteClick, - onRejectTeamInviteClick, + // onAcceptApplyTeamClick, + // onRejectApplyTeamClick, + // onAcceptTeamInviteClick, + // onRejectTeamInviteClick, onAcceptApplyFriendClick, onRejectApplyFriendClick, afterSendMsgClick, @@ -69,21 +66,21 @@ export const MsgItem: FC = observer( const { store } = useStateContext() - const handleRejectApplyTeamClick = () => { - onRejectApplyTeamClick?.(msg as V2NIMTeamJoinActionInfoForUI) - } + // const handleRejectApplyTeamClick = () => { + // onRejectApplyTeamClick?.(msg as V2NIMTeamJoinActionInfoForUI) + // } - const handleAcceptApplyTeamClick = () => { - onAcceptApplyTeamClick?.(msg as V2NIMTeamJoinActionInfoForUI) - } + // const handleAcceptApplyTeamClick = () => { + // onAcceptApplyTeamClick?.(msg as V2NIMTeamJoinActionInfoForUI) + // } - const handleRejectTeamInviteClick = () => { - onRejectTeamInviteClick?.(msg as V2NIMTeamJoinActionInfoForUI) - } + // const handleRejectTeamInviteClick = () => { + // onRejectTeamInviteClick?.(msg as V2NIMTeamJoinActionInfoForUI) + // } - const handleAcceptTeamInviteClick = () => { - onAcceptTeamInviteClick?.(msg as V2NIMTeamJoinActionInfoForUI) - } + // const handleAcceptTeamInviteClick = () => { + // onAcceptTeamInviteClick?.(msg as V2NIMTeamJoinActionInfoForUI) + // } const handleRejectApplyFriendClick = () => { onRejectApplyFriendClick?.(msg as V2NIMFriendAddApplicationForUI) @@ -98,6 +95,7 @@ export const MsgItem: FC = observer( // 自己是否是申请者 const isMeApplicant = applyMsg.applicantAccountId === store.userStore.myUserInfo.accountId + switch (applyMsg.status) { case V2NIMConst.V2NIMFriendAddApplicationStatus .V2NIM_FRIEND_ADD_APPLICATION_STATUS_AGREED: @@ -155,6 +153,9 @@ export const MsgItem: FC = observer( account: applyMsg.applicantAccountId, })} + + {t('applyFriendText')} +
@@ -189,7 +190,7 @@ export const MsgItem: FC = observer( ) case V2NIMConst.V2NIMFriendAddApplicationStatus .V2NIM_FRIEND_ADD_APPLICATION_STATUS_INIT: - return ( + return isMeApplicant ? null : ( <>
= observer( const renderTeamJoinActionMsg = () => { // TODO 暂不支持群相关申请 const teamJoinActionMsg = msg as V2NIMTeamJoinActionInfoForUI + switch (teamJoinActionMsg.actionType) { case V2NIMConst.V2NIMTeamJoinActionType .V2NIM_TEAM_JOIN_ACTION_TYPE_APPLICATION: diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/conversation/Container.tsx index 040fee1..be907aa 100644 --- a/react/src/YXUIKit/im-kit-ui/src/conversation/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/Container.tsx @@ -9,6 +9,7 @@ import { observer } from 'mobx-react' import { V2NIMConversation } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMConversationService' import sdkPkg from 'nim-web-sdk-ng/package.json' import { V2NIMConst } from 'nim-web-sdk-ng' +import { PinAIList } from './components/pinAIList' export interface ConversationContainerProps { /** @@ -97,7 +98,9 @@ export const ConversationContainer: FC = observer( renderConversationName, renderConversationMsg, }) => { - const { nim, store } = useStateContext() + const _prefix = `${prefix}-wrapper` + + const { nim, store, localOptions } = useStateContext() useEventTracking({ appkey: nim.options.appkey, @@ -155,23 +158,30 @@ export const ConversationContainer: FC = observer( }, [store.uiStore.conversations]) return ( - +
+ {localOptions.aiVisible ? ( + + ) : null} + +
) } ) diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/components/ConversationItem.tsx b/react/src/YXUIKit/im-kit-ui/src/conversation/components/ConversationItem.tsx index 8f8a589..3f12bbd 100644 --- a/react/src/YXUIKit/im-kit-ui/src/conversation/components/ConversationItem.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/components/ConversationItem.tsx @@ -43,15 +43,16 @@ export const ConversationItem: FC = ({ conversationNameRenderer, renderConversationMsgIsRead, prefix = 'conversation', - commonPrefix = 'common', }) => { const date = useMemo(() => { if (!time) { return '' } + const _d = moment(time) const isCurrentDay = _d.isSame(moment(), 'day') const isCurrentYear = _d.isSame(moment(), 'year') + return _d.format( isCurrentDay ? 'HH:mm' : isCurrentYear ? 'MM-DD' : 'YYYY-MM' ) @@ -72,15 +73,18 @@ export const ConversationItem: FC = ({ ) { return t('recallMessageText') } + if (messageType === void 0) { return '' } + if ( sendingState === V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING ) { return '' } + if ( sendingState === V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIItem.tsx b/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIItem.tsx new file mode 100644 index 0000000..5260c83 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIItem.tsx @@ -0,0 +1,56 @@ +import { observer } from 'mobx-react' +import React, { FC } from 'react' +import { + ComplexAvatarContainer, + useStateContext, + useTranslation, +} from '../../common' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' +import { message } from 'antd' +import { logger } from '../../utils' +import { V2NIMConst } from 'nim-web-sdk-ng' + +export interface PinAIItemProps { + aiUser: V2NIMAIUser + + prefix?: string + commonPrefix?: string +} + +export const PinAIItem: FC = observer( + ({ aiUser, prefix = 'conversation', commonPrefix = 'common' }) => { + const _prefix = `${prefix}-ai-item` + + const { store } = useStateContext() + + const { t } = useTranslation() + + const handleItemClick = () => { + store.conversationStore + .insertConversationActive( + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P, + aiUser.accountId, + true + ) + .catch((err) => { + message.error(t('aiConversationSelectFailed')) + logger.error(err) + }) + } + + return ( +
+ + + {store.uiStore.getAppellation({ + account: aiUser.accountId, + })} + +
+ ) + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIList.tsx b/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIList.tsx new file mode 100644 index 0000000..9779a1b --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/components/pinAIList.tsx @@ -0,0 +1,32 @@ +import { observer } from 'mobx-react' +import React, { FC } from 'react' +import { useStateContext } from '../../common' +import { PinAIItem } from './pinAIItem' + +export interface PinAIListProps { + prefix?: string + commonPrefix?: string +} + +export const PinAIList: FC = observer( + ({ prefix = 'conversation', commonPrefix = 'common' }) => { + const _prefix = `${prefix}-ai-list` + + const { store } = useStateContext() + + const aiUsers = store.aiUserStore.getAIPinUser() + + return aiUsers.length ? ( +
+ {aiUsers.map((aiUser) => ( + + ))} +
+ ) : null + } +) diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.less b/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.less new file mode 100644 index 0000000..889bb97 --- /dev/null +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.less @@ -0,0 +1,153 @@ +@import './theme.less'; + +@conversation-prefix-cls: ~'@{conversation-prefix}-wrapper'; +@conversation-list-prefix-cls: ~'@{conversation-prefix}-list'; +@conversation-item-prefix-cls: ~'@{conversation-prefix}-item'; +@ai-prefix-cls: ~'@{conversation-prefix}-ai'; + +.@{conversation-prefix-cls} { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background-color: @yx-primary-color; +} + +.@{conversation-list-prefix-cls}-loading { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.@{conversation-list-prefix-cls}-wrapper { + width: 100%; + flex: 1; + overflow-y: auto; +} + +.@{conversation-item-prefix-cls} { + width: 100%; + display: flex; + padding: 12px; + background-color: @yx-primary-color; + position: relative; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + align-items: center; + + &-content { + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 0 6px 0 10px; + flex: 1; + min-width: 0; + + &-name { + font-size: @yx-primary-font-size; + color: @yx-primary-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-msg { + font-size: @yx-font-size-12; + color: @yx-text-color-2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; + } + &-msg-body { + font-size: @yx-font-size-12; + color: @yx-text-color-2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &-mention { + font-size: @yx-font-size-12; + color: @yx-text-color-7; + } + &-read-status { + margin-right: 4px; + } + &-read-icon { + font-size: @yx-primary-font-size; + color: @yx-text-color-4 !important; + position: relative; + top: 1px; + } + } + + &-state { + display: flex; + flex-direction: column; + width: fit-content; + align-items: flex-end; + white-space: nowrap; + + &-date { + font-size: @yx-font-size-12; + color: @yx-text-color-2; + } + + &-mute { + color: @yx-icon-color-1; + height: 22px; + } + } + + &-top { + background-color: @yx-primary-color-hover; + } + + &-select { + background-color: @yx-primary-color-active; + } + + &:hover { + background-color: @yx-primary-color-active; + } +} + +.@{ai-prefix-cls} { + &-list { + width: 100%; + max-height: 216px; + overflow-y: auto; + display: flex; + flex-direction: row; + align-items: center; + padding: 6px; + flex-wrap: wrap; + border-bottom: 1px solid @yx-border-color-2; + } + + &-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 6px; + cursor: pointer; + + &:hover { + background-color: @yx-primary-color-active; + } + + &-name { + display: inline-block; + text-align: center; + max-width: 36px; + margin-top: 5px; + font-size: @yx-font-size-12; + color: @yx-text-color-1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} diff --git a/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.ts index b2ab571..83aa197 100644 --- a/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/conversation/style/index.ts @@ -7,5 +7,4 @@ import 'antd/lib/empty/style' import '../../common/components/CrudeAvatar/style' import '../../common/components/CommonParseSession/style' -import './conversationItem.less' -import './conversationList.less' +import './index.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/index.ts b/react/src/YXUIKit/im-kit-ui/src/index.ts index f624de7..7e89bef 100644 --- a/react/src/YXUIKit/im-kit-ui/src/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/index.ts @@ -4,6 +4,7 @@ import { Utils, CrudeAvatar, SearchInput, + SelectModal, CommonIcon, ComplexAvatarUI, ComplexAvatarContainer, @@ -16,9 +17,11 @@ import { MyUserCard, GroupAvatarSelect, urls, - FriendSelectContainer, + FriendSelect, + CreateTeamModal, Welcome, ReadPercent, + RichText, ParseSession, useStateContext, useTranslation, @@ -33,8 +36,10 @@ import { FriendListContainer, GroupListContainer, ContactInfoContainer, + AIListContainer, + MsgListContainer, } from './contact' -import { ChatContainer, ChatMessageItem } from './chat' +import { ChatContainer, ChatMessageItem, ChatCollectionList } from './chat' import { AddContainer, SearchContainer } from './search' import RootStore from '@xkit-yx/im-store-v2' import V2NIM from 'nim-web-sdk-ng' @@ -55,14 +60,18 @@ export class IMUIKit { T extends | typeof ComplexAvatarContainer | typeof MyAvatarContainer - | typeof FriendSelectContainer + | typeof FriendSelect + | typeof CreateTeamModal | typeof ConversationContainer | typeof ContactListContainer | typeof BlackListContainer + | typeof AIListContainer + | typeof MsgListContainer | typeof FriendListContainer | typeof GroupListContainer | typeof ContactInfoContainer | typeof ChatContainer + | typeof ChatCollectionList | typeof AddContainer | typeof SearchContainer >(item: T, props: React.ComponentProps | null, view: HTMLElement): void { @@ -112,21 +121,27 @@ export { MyUserCard, GroupAvatarSelect, urls, - FriendSelectContainer, + FriendSelect, + CreateTeamModal, Welcome, ReadPercent, ParseSession, + RichText, + SelectModal, useStateContext, useTranslation, useEventTracking, ConversationContainer, ContactListContainer, BlackListContainer, + AIListContainer, + MsgListContainer, FriendListContainer, GroupListContainer, ContactInfoContainer, ChatContainer, ChatMessageItem, + ChatCollectionList, AddContainer, SearchContainer, } diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/search/add/Container.tsx index 72ceb01..ce310fd 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/Container.tsx @@ -1,9 +1,12 @@ import React, { useState } from 'react' -import { useStateContext, useEventTracking } from '../../common' +import { + useStateContext, + useEventTracking, + CreateTeamModal, +} from '../../common' import AddPanel from './components/AddPanel' import AddFriendModal from './components/AddFriendModal' import JoinTeamModal from './components/JoinTeamModal' -import CreateModal from './components/CreateModal' import packageJson from '../../../package.json' import { observer } from 'mobx-react' import sdkPkg from 'nim-web-sdk-ng/package.json' @@ -52,6 +55,7 @@ export const AddContainer: React.FC = observer( break case 'joinTeam': setJoinTeamModalVisible(visible) + break default: break } @@ -89,14 +93,13 @@ export const AddContainer: React.FC = observer( prefix={prefix} commonPrefix={commonPrefix} /> - { setCreateModalVisible(false) }} onChat={handleChat.bind(null, 'createTeam')} - prefix={prefix} - commonPrefix={commonPrefix} + prefix={commonPrefix} />
) diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddFriendModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddFriendModal/index.tsx index 90941a2..a30be10 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddFriendModal/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddFriendModal/index.tsx @@ -3,9 +3,9 @@ import { SearchInput, useTranslation, useStateContext, + CrudeAvatar, } from '../../../../common' import React, { useState } from 'react' -import { CrudeAvatar } from '../../../../common' import { V2NIMUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMUserService' import { observer } from 'mobx-react' import { V2NIMConst } from 'nim-web-sdk-ng' @@ -50,11 +50,13 @@ const AddFriendModal: React.FC = observer( try { setSearching(true) const user = await store.userStore.getUserActive(searchValue) + if (!user) { setSearchResEmpty(true) } else { setSearchRes(user) } + setSearching(false) } catch (error) { setSearchResEmpty(true) @@ -80,11 +82,13 @@ const AddFriendModal: React.FC = observer( }) message.success(t('addFriendSuccessText')) } + // 发送申请或添加好友成功后解除黑名单 await store.relationStore.removeUserFromBlockListActive( searchRes.accountId ) } + setAdding(false) } catch (error) { setAdding(false) diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddItem/index.tsx b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddItem/index.tsx index 85a5d0a..342041b 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddItem/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddItem/index.tsx @@ -18,6 +18,7 @@ const AddItem: React.FC = ({ prefix, }) => { const _prefix = `${prefix}-add-item` + return (
onClick(scene)}> @@ -25,4 +26,5 @@ const AddItem: React.FC = ({
) } + export default AddItem diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddList/index.tsx b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddList/index.tsx index 3426d3a..d824513 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddList/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddList/index.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { AddItemProps } from '../AddItem' -import AddItem from '../AddItem' +import AddItem, { AddItemProps } from '../AddItem' + export interface AddListProps { list: Omit[] prefix: string @@ -8,6 +8,7 @@ export interface AddListProps { const AddList: React.FC = ({ list, prefix }) => { const _prefix = `${prefix}-add-list` + return (
{list.map((item) => { diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddPanel/index.tsx b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddPanel/index.tsx index d8c624c..3ef2366 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddPanel/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/components/AddPanel/index.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react' import AddList, { AddListProps } from '../AddList' import { CommonIcon, useTranslation } from '../../../../common' import { PanelScene } from '../../Container' + export interface AddPanelProps { trigger: string // hover || focus || click onClick: (scene: PanelScene) => void @@ -52,6 +53,7 @@ const AddPanel: React.FC = ({
) + return (
= observer( try { setSearching(true) const team = await store.teamStore.getTeamForceActive(searchValue) + if (!team) { setSearchResEmpty(true) } else { setSearchRes(team) } + setSearching(false) } catch (error) { setSearchResEmpty(true) @@ -72,13 +74,16 @@ const JoinTeamModal: React.FC = observer( message.error(t('notSupportJoinText')) return } + setAdding(true) await store.teamStore.applyTeamActive(searchRes.teamId) // 目前没有申请加入群组,直接写死 message.success(t('joinTeamSuccessText')) } + setAdding(false) } catch (error) { + message.error(t('joinTeamFailedText')) setAdding(false) } } diff --git a/react/src/YXUIKit/im-kit-ui/src/search/add/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/search/add/style/index.ts index c9fdb0a..659de25 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/add/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/search/add/style/index.ts @@ -4,12 +4,12 @@ import 'antd/lib/button/style' import 'antd/lib/message/style' import 'antd/lib/input/style' -import '../../../common/components/GroupAvatarSelect/style' import '../../../common/components/FriendSelect/style' import '../../../common/components/SearchInput/style' +import '../../../common/components/CreateTeamModal/style' +import '../../../common/components/CrudeAvatar/style' import './addItem.less' import './addList.less' import './addModal.less' import './addPanel.less' -import './createModal.less' diff --git a/react/src/YXUIKit/im-kit-ui/src/search/search/Container.tsx b/react/src/YXUIKit/im-kit-ui/src/search/search/Container.tsx index 29dbb10..4e84be4 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/search/Container.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/search/Container.tsx @@ -1,8 +1,7 @@ import React, { useState } from 'react' import { useTranslation, useEventTracking, useStateContext } from '../../common' import { SearchOutlined } from '@ant-design/icons' -import SearchModal from './components/SearchModal' -import { SectionListItem } from './components/SearchModal' +import SearchModal, { SectionListItem } from './components/SearchModal' import packageJson from '../../../package.json' import { V2NIMTeam } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMTeamService' import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMConversationService' @@ -63,6 +62,7 @@ export const SearchContainer: React.FC = observer( const handleChat = async (item: SectionListItem) => { let conversationType: V2NIMConversationType let receiverId = '' + if ((item as V2NIMFriend & V2NIMUser).accountId) { conversationType = V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P @@ -97,6 +97,7 @@ export const SearchContainer: React.FC = observer( name: '', createTime: Date.now(), } + return { ...item, ...user, diff --git a/react/src/YXUIKit/im-kit-ui/src/search/search/components/SearchModal/index.tsx b/react/src/YXUIKit/im-kit-ui/src/search/search/components/SearchModal/index.tsx index 0c3f6d4..74476ec 100644 --- a/react/src/YXUIKit/im-kit-ui/src/search/search/components/SearchModal/index.tsx +++ b/react/src/YXUIKit/im-kit-ui/src/search/search/components/SearchModal/index.tsx @@ -97,6 +97,7 @@ const SearchModal: React.FC = ({ }), } } + if (item.id === 'groups') { return { ...item, @@ -107,6 +108,7 @@ const SearchModal: React.FC = ({ }), } } + return { ...item } }) .filter((item) => !!item.list.length) diff --git a/react/src/YXUIKit/im-kit-ui/src/style/index.ts b/react/src/YXUIKit/im-kit-ui/src/style/index.ts index 6a5d42f..174a153 100644 --- a/react/src/YXUIKit/im-kit-ui/src/style/index.ts +++ b/react/src/YXUIKit/im-kit-ui/src/style/index.ts @@ -1,4 +1,5 @@ import '../common/components/FriendSelect/style' +import '../common/components/CreateTeamModal/style' import '../common/components/GroupAvatarSelect/style' import '../common/components/CrudeAvatar/style' import '../common/components/ComplexAvatar/style' @@ -10,12 +11,15 @@ import '../common/components/UserCard/style' import '../common/components/SearchInput/style' import '../common/components/ReadPercent/style' import '../common/components/SelectModal/style' +import '../common/components/RichText/style' import '../conversation/style' import '../contact/black-list/style' import '../contact/friend-list/style' import '../contact/group-list/style' import '../contact/contact-list/style' +import '../contact/ai-list/style' +import '../contact/msg-list/style' import '../contact/contact-info/style' import '../chat/style' import '../search/add/style' diff --git a/react/src/YXUIKit/im-kit-ui/src/uploadingTask.ts b/react/src/YXUIKit/im-kit-ui/src/uploadingTask.ts index cf5957e..e409a71 100644 --- a/react/src/YXUIKit/im-kit-ui/src/uploadingTask.ts +++ b/react/src/YXUIKit/im-kit-ui/src/uploadingTask.ts @@ -15,6 +15,7 @@ export const removeTask = (id): boolean => { export const abortTask = (id: string): void => { const task = taskMap.get(id) + if (task) { task.abort() removeTask(id) diff --git a/react/src/YXUIKit/im-kit-ui/src/urlToBlob.ts b/react/src/YXUIKit/im-kit-ui/src/urlToBlob.ts index 1518087..dd68eb0 100644 --- a/react/src/YXUIKit/im-kit-ui/src/urlToBlob.ts +++ b/react/src/YXUIKit/im-kit-ui/src/urlToBlob.ts @@ -3,6 +3,7 @@ export const blobImgMap: { [key: string]: string } = {} export const urlToBlob = async (url: string): Promise => { const res = await fetch(url) const blob = await res.blob() + return URL.createObjectURL(blob) } @@ -10,7 +11,9 @@ export const getBlobImg = async (url: string): Promise => { if (blobImgMap[url]) { return blobImgMap[url] } + const blobUrl = await urlToBlob(url) + blobImgMap[url] = blobUrl return blobUrl } diff --git a/react/src/YXUIKit/im-kit-ui/src/utils.ts b/react/src/YXUIKit/im-kit-ui/src/utils.ts index f8d0dfb..0b16501 100644 --- a/react/src/YXUIKit/im-kit-ui/src/utils.ts +++ b/react/src/YXUIKit/im-kit-ui/src/utils.ts @@ -1,5 +1,7 @@ import { logDebug } from '@xkit-yx/utils' +import moment from 'moment' import packageJson from '../package.json' +import { V2NIMMessage } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMMessageService' export { logDebug } @@ -26,9 +28,11 @@ export function mergeArrs( // @ts-ignore const exist = map.get(item[key]) let finalItem = item + if (exist) { finalItem = { ...exist, ...item } } + // @ts-ignore map.set(item[key], finalItem) }) @@ -44,6 +48,7 @@ export const mergeActions = ( ): T[] => { return propsActions.map((i) => { const defaultAction = defaultActions.find((j) => i[key] === j[key]) + if (defaultAction) { return { ...defaultAction, @@ -69,6 +74,7 @@ export const groupByPy = ( const add = (k: string, v: T) => { const _k = isLowerCase ? k.toLowerCase() : k.toUpperCase() + if (!res[_k]) { res[_k] = [v] } else { @@ -81,8 +87,10 @@ export const groupByPy = ( item[keys.firstKey] || item[keys.secondKey || ''] || item[keys.thirdKey || ''] + if (!!v && typeof v === 'string') { const str = v[0] + if (/^[a-zA-Z]$/.test(str)) { add(str.toLowerCase(), item) } else if (/^[\u4e00-\u9fa5]$/.test(str)) { @@ -93,6 +101,7 @@ export const groupByPy = ( (!zh[ki - 1] || zh[ki - 1].localeCompare(str, 'zh') <= 0) && str.localeCompare(zh[ki], 'zh') == -1 ) + if (k && k !== '*') { add(k, item) } else { @@ -130,6 +139,7 @@ export const frequencyControl = < return function (args) { return new Promise((resolve, reject) => { const p = promiseQueue.find((item) => item.args === args) + if (p) { p.queue.push({ resolve, reject }) } else { @@ -146,6 +156,7 @@ export const frequencyControl = < if (!pq.length) { return } + requesting = true fn.call( // @ts-ignore @@ -155,8 +166,10 @@ export const frequencyControl = < .then((res) => { while (pq.length) { const p = pq.shift() + if (p) { const _ = res.find((j) => j.account === p.args) + p.queue.forEach((j) => j.resolve(_)) } } @@ -164,6 +177,7 @@ export const frequencyControl = < .catch((err) => { while (pq.length) { const p = pq.shift() + if (p) { p.queue.forEach((item) => item.reject(err)) } @@ -233,7 +247,7 @@ export const handleEmojiTranslate = (t) => { [t('ill')]: 'icon-a-34', [t('Mad')]: 'icon-a-35', [t('Ghost')]: 'icon-a-36', - [t('Angry')]: 'icon-a-37', + [t('huff')]: 'icon-a-37', [t('Angry')]: 'icon-a-38', [t('Unhappy')]: 'icon-a-39', [t('Frown')]: 'icon-a-40', @@ -261,7 +275,7 @@ export const handleEmojiTranslate = (t) => { [t('NoWords')]: 'icon-a-62', [t('Monkey')]: 'icon-a-63', [t('Bomb')]: 'icon-a-64', - [t('Sleep')]: 'icon-a-65', + [t('Sleeping')]: 'icon-a-65', [t('Cloud')]: 'icon-a-66', [t('Rocket')]: 'icon-a-67', [t('Ambulance')]: 'icon-a-68', @@ -275,12 +289,14 @@ export const handleEmojiTranslate = (t) => { const left = `\\${item.slice(0, 1)}` const right = `\\${item.slice(-1)}` const mid = item.slice(1, -1) + return `${left}${mid}${right}` }) .join('|') + ')', 'g' ) + return { EMOJI_ICON_MAP_CONFIG, INPUT_EMOJI_SYMBOL_REG, @@ -290,12 +306,15 @@ export const handleEmojiTranslate = (t) => { export function getImgDataUrl(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() + reader.onload = (e) => { resolve(e.target?.result as string) } + reader.onerror = (e) => { reject(e) } + reader.readAsDataURL(file) }) } @@ -311,6 +330,7 @@ export function getVideoFirstFrameDataUrl(videoFile: File): Promise { canvas.height = video.videoHeight context?.drawImage(video, 0, 0, canvas.width, canvas.height) const dataURL = canvas.toDataURL('image/jpeg') + resolve(dataURL) } @@ -319,9 +339,59 @@ export function getVideoFirstFrameDataUrl(videoFile: File): Promise { } const url = URL.createObjectURL(videoFile) + video.preload = 'auto' video.autoplay = true video.muted = true video.src = url }) } + +export const formatDate = (time: number): string => { + const date = moment(time) + const isCurrentDay = date.isSame(moment(), 'day') + const isCurrentYear = date.isSame(moment(), 'year') + + return isCurrentDay + ? date.format('HH:mm:ss') + : isCurrentYear + ? date.format('MM-DD HH:mm:ss') + : date.format('YYYY-MM-DD HH:mm:ss') +} + +export const hasQueryParams = (url: string): boolean => { + try { + const parsedUrl = new URL(url) + + return parsedUrl.search !== '' + } catch (error) { + return false + } +} + +export const getDownloadUrl = (msg: V2NIMMessage) => { + return hasQueryParams(msg.attachment.url) + ? `${msg.attachment.url}&download=${msg.messageClientId}${msg.attachment.ext}` + : `${msg.attachment.url}?download=${msg.messageClientId}${msg.attachment.ext}` +} + +export const getAIErrorMap = (t): { [key: number]: string } => { + return { + 102404: t('memberNotExistsText'), + 189308: t('tipAIFailedMessageText'), + 189451: t('aiAntiSpamText'), + 107337: t('aiFunctionDisabled'), + 102422: t('aiMemberBanned'), + 102421: t('aiMemberChatBanned'), + 104404: t('aiFriendNotExists'), + 107451: t('aiMessageHitAntiSpam'), + 102304: t('notAnAi'), + 109404: t('aiTeamMemberNotExists'), + 108306: t('aiNormalTeamChatBanned'), + 109424: t('aiTeamChatBanned'), + 106403: t('aiBlockFailedText'), + 416: t('aiRateLimit'), + 414: t('aiParameterError'), + 107336: t('tipAIMessageText'), + } +} diff --git a/react/src/components/IMApp/iconfont.css b/react/src/components/IMApp/iconfont.css index 88e2648..5f57287 100644 --- a/react/src/components/IMApp/iconfont.css +++ b/react/src/components/IMApp/iconfont.css @@ -1,13 +1,16 @@ +/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ @font-face { - font-family: "iconfont"; /* Project id 3429868 */ - src: - url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), - url('//at.alicdn.com/t/c/font_3429868_zbnnmxh96v.woff?t=1681118915377') format('woff'), - url('//at.alicdn.com/t/c/font_3429868_zbnnmxh96v.ttf?t=1681118915377') format('truetype'); + font-family: 'iconfont'; /* Project id 3429868 */ + src: url('data:application/x-font-woff2;charset=utf-8;base64,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') + format('woff2'), + url('//at.alicdn.com/t/c/font_3429868_c7j6dxqx62m.woff?t=1716800647511') + format('woff'), + url('//at.alicdn.com/t/c/font_3429868_c7j6dxqx62m.ttf?t=1716800647511') + format('truetype'); } .iconfont { - font-family: "iconfont" !important; + font-family: 'iconfont' !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; @@ -15,529 +18,529 @@ } .icon-shipin8:before { - content: "\e735"; + content: '\e735'; } .icon-shipinyuyin:before { - content: "\e736"; + content: '\e736'; } .icon-yuyin8:before { - content: "\e737"; + content: '\e737'; } .icon-kefu:before { - content: "\e625"; + content: '\e625'; } .icon-tuigejian:before { - content: "\e734"; + content: '\e734'; } .icon-shezhi1:before { - content: "\e658"; + content: '\e658'; } .icon-team:before { - content: "\e75a"; + content: '\e75a'; } .icon-More:before { - content: "\e60a"; + content: '\e60a'; } .icon-a-Frame7:before { - content: "\e732"; + content: '\e732'; } .icon-a-Frame8:before { - content: "\e733"; + content: '\e733'; } .icon-zuojiantou:before { - content: "\e731"; + content: '\e731'; } .icon-guanyu:before { - content: "\e633"; + content: '\e633'; } .icon-shandiao:before { - content: "\e64e"; + content: '\e64e'; } .icon-addition:before { - content: "\e730"; + content: '\e730'; } .icon-fuzhi1:before { - content: "\e61b"; + content: '\e61b'; } .icon-huifu:before { - content: "\e72f"; + content: '\e72f'; } .icon-jiantou:before { - content: "\e72d"; + content: '\e72d'; } .icon-zhankai:before { - content: "\e72e"; + content: '\e72e'; } .icon-weidu:before { - content: "\e723"; + content: '\e723'; } .icon-yidu:before { - content: "\e724"; + content: '\e724'; } .icon-sifenzhiyiyidu:before { - content: "\e725"; + content: '\e725'; } .icon-erfenzhiyiyidu:before { - content: "\e726"; + content: '\e726'; } .icon-sifenzhisanyidu:before { - content: "\e727"; + content: '\e727'; } .icon-zhongyingwen:before { - content: "\e728"; + content: '\e728'; } .icon-tuichudenglu:before { - content: "\e729"; + content: '\e729'; } .icon-yuyin1:before { - content: "\e72a"; + content: '\e72a'; } .icon-yuyin2:before { - content: "\e72b"; + content: '\e72b'; } .icon-yuyin3:before { - content: "\e72c"; + content: '\e72c'; } .icon-Excel:before { - content: "\e721"; + content: '\e721'; } .icon-shipin:before { - content: "\e722"; + content: '\e722'; } .icon-a-68:before { - content: "\e6dc"; + content: '\e6dc'; } .icon-a-64:before { - content: "\e6dd"; + content: '\e6dd'; } .icon-a-65:before { - content: "\e6de"; + content: '\e6de'; } .icon-a-66:before { - content: "\e6df"; + content: '\e6df'; } .icon-a-70:before { - content: "\e6e0"; + content: '\e6e0'; } .icon-a-63:before { - content: "\e6e1"; + content: '\e6e1'; } .icon-a-67:before { - content: "\e6e2"; + content: '\e6e2'; } .icon-a-59:before { - content: "\e6e3"; + content: '\e6e3'; } .icon-a-56:before { - content: "\e6e4"; + content: '\e6e4'; } .icon-a-40:before { - content: "\e6e5"; + content: '\e6e5'; } .icon-a-32:before { - content: "\e6e6"; + content: '\e6e6'; } .icon-a-36:before { - content: "\e6e7"; + content: '\e6e7'; } .icon-a-60:before { - content: "\e6e8"; + content: '\e6e8'; } .icon-a-58:before { - content: "\e6e9"; + content: '\e6e9'; } .icon-a-52:before { - content: "\e6ea"; + content: '\e6ea'; } .icon-a-61:before { - content: "\e6eb"; + content: '\e6eb'; } .icon-a-55:before { - content: "\e6ec"; + content: '\e6ec'; } .icon-a-31:before { - content: "\e6ed"; + content: '\e6ed'; } .icon-a-57:before { - content: "\e6ee"; + content: '\e6ee'; } .icon-a-34:before { - content: "\e6ef"; + content: '\e6ef'; } .icon-a-38:before { - content: "\e6f0"; + content: '\e6f0'; } .icon-a-54:before { - content: "\e6f1"; + content: '\e6f1'; } .icon-a-62:before { - content: "\e6f2"; + content: '\e6f2'; } .icon-a-51:before { - content: "\e6f3"; + content: '\e6f3'; } .icon-a-37:before { - content: "\e6f4"; + content: '\e6f4'; } .icon-a-49:before { - content: "\e6f5"; + content: '\e6f5'; } .icon-a-28:before { - content: "\e6f6"; + content: '\e6f6'; } .icon-a-45:before { - content: "\e6f7"; + content: '\e6f7'; } .icon-a-41:before { - content: "\e6f8"; + content: '\e6f8'; } .icon-a-23:before { - content: "\e6f9"; + content: '\e6f9'; } .icon-a-39:before { - content: "\e6fa"; + content: '\e6fa'; } .icon-a-29:before { - content: "\e6fb"; + content: '\e6fb'; } .icon-a-13:before { - content: "\e6fc"; + content: '\e6fc'; } .icon-a-35:before { - content: "\e6fd"; + content: '\e6fd'; } .icon-a-5:before { - content: "\e6fe"; + content: '\e6fe'; } .icon-a-11:before { - content: "\e6ff"; + content: '\e6ff'; } .icon-a-48:before { - content: "\e700"; + content: '\e700'; } .icon-a-10:before { - content: "\e701"; + content: '\e701'; } .icon-a-43:before { - content: "\e702"; + content: '\e702'; } .icon-a-7:before { - content: "\e703"; + content: '\e703'; } .icon-a-12:before { - content: "\e704"; + content: '\e704'; } .icon-a-2:before { - content: "\e705"; + content: '\e705'; } .icon-a-33:before { - content: "\e706"; + content: '\e706'; } .icon-a-15:before { - content: "\e707"; + content: '\e707'; } .icon-a-17:before { - content: "\e708"; + content: '\e708'; } .icon-a-44:before { - content: "\e709"; + content: '\e709'; } .icon-a-20:before { - content: "\e70a"; + content: '\e70a'; } .icon-a-4:before { - content: "\e70b"; + content: '\e70b'; } .icon-a-42:before { - content: "\e70c"; + content: '\e70c'; } .icon-a-30:before { - content: "\e70d"; + content: '\e70d'; } .icon-a-9:before { - content: "\e70e"; + content: '\e70e'; } .icon-a-25:before { - content: "\e70f"; + content: '\e70f'; } .icon-a-46:before { - content: "\e710"; + content: '\e710'; } .icon-a-21:before { - content: "\e711"; + content: '\e711'; } .icon-a-24:before { - content: "\e712"; + content: '\e712'; } .icon-a-3:before { - content: "\e713"; + content: '\e713'; } .icon-a-8:before { - content: "\e714"; + content: '\e714'; } .icon-a-22:before { - content: "\e715"; + content: '\e715'; } .icon-a-1:before { - content: "\e716"; + content: '\e716'; } .icon-a-53:before { - content: "\e717"; + content: '\e717'; } .icon-a-18:before { - content: "\e718"; + content: '\e718'; } .icon-a-50:before { - content: "\e719"; + content: '\e719'; } .icon-a-6:before { - content: "\e71a"; + content: '\e71a'; } .icon-a-26:before { - content: "\e71b"; + content: '\e71b'; } .icon-a-47:before { - content: "\e71c"; + content: '\e71c'; } .icon-a-27:before { - content: "\e71d"; + content: '\e71d'; } .icon-a-14:before { - content: "\e71e"; + content: '\e71e'; } .icon-a-16:before { - content: "\e71f"; + content: '\e71f'; } .icon-a-19:before { - content: "\e720"; + content: '\e720'; } .icon-lahei:before { - content: "\e6db"; + content: '\e6db'; } .icon-wenjian:before { - content: "\e6d8"; + content: '\e6d8'; } .icon-tupian:before { - content: "\e6d9"; + content: '\e6d9'; } .icon-biaoqing:before { - content: "\e6da"; + content: '\e6da'; } .icon-shezhi:before { - content: "\e6d6"; + content: '\e6d6'; } .icon-lishixiaoxi:before { - content: "\e6d7"; + content: '\e6d7'; } .icon-chehui:before { - content: "\e6d5"; + content: '\e6d5'; } .icon-tianjiaanniu:before { - content: "\e6d4"; + content: '\e6d4'; } .icon-jiaruqunzu:before { - content: "\e6d3"; + content: '\e6d3'; } .icon-chuangjianqunzu:before { - content: "\e6d1"; + content: '\e6d1'; } .icon-tianjiahaoyou:before { - content: "\e6d2"; + content: '\e6d2'; } .icon-sousuo:before { - content: "\e6d0"; + content: '\e6d0'; } .icon-touxiang3:before { - content: "\e6cc"; + content: '\e6cc'; } .icon-touxiang1:before { - content: "\e6cd"; + content: '\e6cd'; } .icon-touxiang2:before { - content: "\e6ce"; + content: '\e6ce'; } .icon-touxiang4:before { - content: "\e6cf"; + content: '\e6cf'; } .icon-touxiang5:before { - content: "\e6cb"; + content: '\e6cb'; } .icon-shanchu:before { - content: "\e6c5"; + content: '\e6c5'; } .icon-quxiaoxiaoximiandarao:before { - content: "\e6ca"; + content: '\e6ca'; } .icon-tongxunlu-xuanzhong:before { - content: "\e6c3"; + content: '\e6c3'; } .icon-tongxunlu-weixuanzhong:before { - content: "\e6c4"; + content: '\e6c4'; } .icon-quxiaozhiding:before { - content: "\e6c6"; + content: '\e6c6'; } .icon-xiaoxizhiding:before { - content: "\e6c7"; + content: '\e6c7'; } .icon-im-xuanzhong:before { - content: "\e6c8"; + content: '\e6c8'; } .icon-im:before { - content: "\e6c9"; + content: '\e6c9'; } .icon-Word:before { - content: "\e6ba"; + content: '\e6ba'; } .icon-yinle:before { - content: "\e6bc"; + content: '\e6bc'; } .icon-RAR1:before { - content: "\e6bd"; + content: '\e6bd'; } .icon-PPT:before { - content: "\e6be"; + content: '\e6be'; } .icon-tupian2:before { - content: "\e6bf"; + content: '\e6bf'; } .icon-weizhiwenjian:before { - content: "\e6c0"; + content: '\e6c0'; } .icon-qita:before { - content: "\e6c1"; + content: '\e6c1'; } .icon-xiaoximiandarao:before { - content: "\e6c2"; + content: '\e6c2'; } diff --git a/react/src/components/IMApp/index.less b/react/src/components/IMApp/index.less index cb02c13..7bee284 100644 --- a/react/src/components/IMApp/index.less +++ b/react/src/components/IMApp/index.less @@ -70,6 +70,7 @@ body{ } .chat-icon, + .collection-icon, .contact-icon { margin: 0 0 25px 0; font-size: 22px; diff --git a/react/src/components/IMApp/index.tsx b/react/src/components/IMApp/index.tsx index 8524a86..2e15a9a 100644 --- a/react/src/components/IMApp/index.tsx +++ b/react/src/components/IMApp/index.tsx @@ -3,6 +3,7 @@ import { Provider, // 全局上下文 ConversationContainer, // 会话列表组件 ChatContainer, // 聊天(会话消息)组件 + ChatCollectionList, AddContainer, // 搜索——添加按钮组件 SearchContainer, // 搜索——搜索组件 ContactListContainer, // 通讯录——通讯录导航组件 @@ -10,6 +11,7 @@ import { MyAvatarContainer, useStateContext, ComplexAvatarContainer, + Utils, } from '@xkit-yx/im-kit-ui/src' import { ConfigProvider, @@ -43,7 +45,7 @@ import { import '@xkit-yx/call-kit-react-ui/es/style' import Calling from './components/call' //demo国际化函数 -import { convertSecondsToTime, g2StatusMap, renderMsgDate, t } from './util' +import { convertSecondsToTime, g2StatusMap, t } from './util' import { DeleteOutlined } from '@ant-design/icons' import { pauseAllAudio, @@ -51,6 +53,7 @@ import { } from '@xkit-yx/im-kit-ui/src/common/components/CommonParseSession' import { LocalOptions } from '@xkit-yx/im-store-v2/dist/types/types' import V2NIM, { V2NIMConst } from 'nim-web-sdk-ng' +import { V2NIMAIUser } from 'nim-web-sdk-ng/dist/v2/NIM_BROWSER_SDK/V2NIMAIService' import { RenderP2pCustomMessageOptions } from '@xkit-yx/im-kit-ui/src/chat/components/ChatP2pMessageList' interface IMContainerProps { @@ -87,14 +90,14 @@ const IMApp: React.FC = observer((props) => { teamManagerVisible, } = props const callViewProviderRef = useRef(null) - const [model, setModel] = useState<'chat' | 'contact'>('chat') + const [model, setModel] = useState<'chat' | 'contact' | 'collection'>('chat') const [isSettingModalOpen, setIsSettingModalOpen] = useState(false) // 是否显示呼叫弹窗 const [callingVisible, setCallingVisible] = useState(false) // IMUIKit store 与 nim sdk 实例 const { store, nim } = useStateContext() - const conversationId = store.uiStore.selectedConversation + // const conversationId = store.uiStore.selectedConversation const conversationType = nim.V2NIMConversationIdUtil.parseConversationType( store.uiStore.selectedConversation ) @@ -102,11 +105,14 @@ const IMApp: React.FC = observer((props) => { store.uiStore.selectedConversation ) + const { relation } = store.uiStore.getRelation(receiverId) + const messageActionDropdownContainerRef = useRef(null) const handleSettingCancel = () => { setIsSettingModalOpen(false) } + const openSettingModal = () => { setIsSettingModalOpen(true) } @@ -173,11 +179,13 @@ const IMApp: React.FC = observer((props) => { action: 'sendFile', visible: true, }, + { action: 'aiTranslate' }, { action: 'calling', visible: conversationType === - V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P, + V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P && + relation !== 'ai', render: () => { return (
@@ -311,6 +322,17 @@ const IMApp: React.FC = observer((props) => {
{t('session')}
+ +
setModel('collection')} + > + +
{t('collectionText')}
+
+
= observer((props) => { const textMsg = nim.V2NIMMessageCreator.createTextMessage( t('passFriendAskText') ) + store.msgStore .sendMessageActive({ msg: textMsg, @@ -382,6 +405,7 @@ const IMApp: React.FC = observer((props) => {
)} + {model === 'collection' && }
) @@ -439,6 +463,7 @@ const IMAppContainer: React.FC = (props) => { // 是否开启群管理员功能 const [teamManagerVisible, setTeamManagerVisible] = useState(true) const languageMap = useMemo(() => ({ zh, en }), []) + // 本地默认行为参数 const localOptions: Partial = useMemo(() => { return { @@ -459,6 +484,31 @@ const IMAppContainer: React.FC = (props) => { loginStateVisible: false, // 是否允许转让群主 allowTransferTeamOwner: true, + // AI 功能是否开启 + aiVisible: true, + // AI 提供者 + aiUserAgentProvider: { + /** + * 注册 AI 划词数字人 + */ + getAISearchUser: (users: V2NIMAIUser[]): V2NIMAIUser | void => { + // demo 根据 accid 匹配,具体值根据业务后台配置的来 + return users.find((item) => item.accountId === 'search') + }, + /** + * 注册 AI 翻译数字人 + */ + getAITranslateUser: (users: V2NIMAIUser[]): V2NIMAIUser | void => { + // demo 根据 accid 匹配,具体值根据业务后台配置的来 + return users.find((item) => item.accountId === 'translation') + }, + /** + * 注册 AI 翻译语言 + */ + getAITranslateLangs: (users: V2NIMAIUser[]): string[] => { + return ['英语', '日语', '韩语', '俄语', '法语', '德语'] + }, + }, } }, [ addFriendNeedVerify, @@ -483,19 +533,24 @@ const IMAppContainer: React.FC = (props) => { ) const _needMention = sessionStorage.getItem('needMention') const _teamManagerVisible = sessionStorage.getItem('teamManagerVisible') + setCurLanguage(_languageType || 'zh') if (_p2pMsgReceiptVisible) { setP2pMsgReceiptVisible(_p2pMsgReceiptVisible === 'true') } + if (_teamMsgReceiptVisible) { setTeamMsgReceiptVisible(_teamMsgReceiptVisible === 'true') } + if (_addFriendNeedVerify) { setAddFriendNeedVerify(_addFriendNeedVerify === 'true') } + if (_needMention) { setNeedMention(_needMention === 'true') } + if (_teamManagerVisible) { setTeamManagerVisible(_teamManagerVisible === 'true') } @@ -509,6 +564,7 @@ const IMAppContainer: React.FC = (props) => { debugLevel: 'debug', apiVersion: 'v2', }) + return nim }, [account, token, appkey]) diff --git a/react/src/components/IMApp/locales/demo_locale.ts b/react/src/components/IMApp/locales/demo_locale.ts index b81f569..183284c 100644 --- a/react/src/components/IMApp/locales/demo_locale.ts +++ b/react/src/components/IMApp/locales/demo_locale.ts @@ -27,6 +27,7 @@ export const demo_zh = { needMentionText: '是否需要@消息', teamManagerEnableText: '是否开启群管理员功能', deleteText: '删除', + collectionText: '收藏', } export const demo_en = { @@ -59,4 +60,5 @@ export const demo_en = { needMentionText: 'Whether or not an @ message is needed', teamManagerEnableText: 'Whether or not team manager is needed', deleteText: 'delete', + collectionText: 'collection', } diff --git a/react/src/components/IMApp/locales/en.ts b/react/src/components/IMApp/locales/en.ts index cf4146b..a82422b 100644 --- a/react/src/components/IMApp/locales/en.ts +++ b/react/src/components/IMApp/locales/en.ts @@ -2,321 +2,304 @@ const LocaleConfig = { // common saveText: 'Save', setText: 'Settings', - saveSuccessText: 'Settings saved', - saveFailedText: 'Failed to save Settings', - addFriendSuccessText: 'Friend added', - applyFriendSuccessText: 'Friend request sent', - addFriendFailedText: 'Failed to add friend', - applyFriendFailedText: 'Failed to send friend request', + saveSuccessText: 'Save Successful', + saveFailedText: 'Save Failed', + addFriendSuccessText: 'Add Friend Successful', + applyFriendSuccessText: 'Friend Request Sent Successfully', + addFriendFailedText: 'Add Friend Failed', + applyFriendFailedText: 'Friend Request Failed', okText: 'OK', cancelText: 'Cancel', deleteText: 'Delete', recallText: 'Recall', forwardText: 'Forward', - forwardFailedText: 'Forward failed', - sendBtnText: 'send', + forwardSuccessText: 'Forward Successful', + forwardFailedText: 'Forward Failed', + recentForwardText: 'Recent Forward', + sendBtnText: 'Send', replyText: 'Reply', - commentText: 'comment', - recentSessionText: 'Recent session', - you: 'you', - deleteFriendText: 'Delete friend', - confirmDeleteText: 'Delete friend', - confirmDeleteFriendText: 'Are you sure you want to delete the friend?', - deleteFriendSuccessText: 'Friend deleted', - deleteFriendFailedText: 'Failed to delete friend', - blackText: 'Blocked', - removeBlackText: 'Remove from blocklist', - blackSuccessText: 'Blocked', - blackFailedText: 'Failed to block user', - removeBlackSuccessText: 'Unblocked', - removeBlackFailedText: 'Failed to unblock user', - maxSelectedText: 'A maximum of {} friends can be selected', + commentText: 'Comment', + recentSessionText: 'Recent Sessions', + you: 'You', + deleteFriendText: 'Delete Friend', + confirmDeleteText: 'Confirm Deletion?', + confirmDeleteFriendText: 'Confirm Delete Friend?', + deleteFriendSuccessText: 'Delete Friend Successful', + deleteFriendFailedText: 'Delete Friend Failed', + blackText: 'Block Friend', + removeBlackText: 'Unblock', + blackSuccessText: 'Block Successful', + blackFailedText: 'Block Failed', + removeBlackSuccessText: 'Unblock Successful', + removeBlackFailedText: 'Unblock Failed', + maxSelectedText: 'Maximum Selection', selectedText: 'Selected', - friendsText: 'Friends', - strangerText: 'stranger(s)', - emailErrorText: 'Invalid email address', - uploadLimitText: 'A maximum size of {} MB is allowed for upload', + strangerText: 'Strangers', + emailErrorText: 'Invalid Email Format', + uploadLimitText: 'Maximum Upload Size', uploadLimitUnit: 'M', - uploadImgFailedText: 'Upload failed', + uploadImgFailedText: 'Upload Image Failed', accountText: 'Account', nickText: 'Nickname', genderText: 'Gender', phoneText: 'Phone', emailText: 'Email', - signText: 'Bio', - accountPlaceholder: 'Enter your account', - teamIdPlaceholder: 'Please enter the group ID', - nickPlaceholder: 'Enter your nickname', - genderPlaceholder: 'Select your gender', - phonePlaceholder: 'Enter mobile phone number', - emailPlaceholder: 'Enter email address', - signPlaceholder: 'Enter your bio', - searchInputPlaceholder: 'Search for friends or groups', - searchTeamMemberPlaceholder: 'Search for team member', - searchText: 'search', + signText: 'Signature', + accountPlaceholder: 'Please Enter Account', + teamIdPlaceholder: 'Please Enter Group ID', + nickPlaceholder: 'Please Enter Nickname', + genderPlaceholder: 'Please Select Gender', + phonePlaceholder: 'Please Enter Phone Number', + emailPlaceholder: 'Please Enter Email', + signPlaceholder: 'Please Enter Signature', + searchInputPlaceholder: 'Search Friends or Groups', + searchTeamMemberPlaceholder: 'Search Group Members', + searchText: 'Search', man: 'Male', woman: 'Female', - unknow: 'Prefer not to Answer', - welcomeText: 'Welcome to CommsEase Messenger', - notSupportMessageText: 'The type of message is not supported', - applyTeamText: 'Join', - applyTeamSuccessText: 'Apply team success', + unknow: 'Unknown', + welcomeText: 'Welcome to Yunxin', + notSupportMessageText: 'Message Not Supported', + applyTeamText: 'Apply to Join Group', + applyTeamSuccessText: 'Application to Join Group Successful', rejectText: 'Reject', acceptText: 'Accept', - inviteTeamText: 'Invite', - applyFriendText: 'Add', + inviteTeamText: 'Invite You to Join Group', + applyFriendText: 'Add You as Friend', acceptResultText: 'Accepted', rejectResultText: 'Rejected', - beRejectResultText: 'Friend request rejected', - passResultText: 'Friend request accepted', - rejectTeamInviteText: 'declined group invitation', - updateTeamAvatar: 'Updated group avatar', - updateTeamName: 'The group name was updated as', - updateTeamIntro: 'Updated the group introduction', - updateTeamInviteMode: 'Updated the "Invite permission" to', - updateTeamUpdateTeamMode: 'Updated the "Edit permission" to​', - updateAllowAt: 'Updated the "@everyone permission" to', - updateTeamMute: 'group ban', - onlyTeamOwner: 'Only the owner', + expiredResultText: 'Expired', + beRejectResultText: 'Friend Request Rejected', + passResultText: 'Friend Request Accepted', + rejectTeamInviteText: 'Group Invite Rejected', + updateTeamAvatar: 'Updated Group Avatar', + updateTeamName: 'Updated Group Name to', + updateTeamIntro: 'Updated Group Introduction', + updateTeamInviteMode: 'Updated Group Permission "Invite Others" to', + updateTeamUpdateTeamMode: 'Updated Group Permission "Edit Group Info" to', + updateAllowAt: 'Updated "@Everyone Permission" to', + updateAllowTop: 'Pin Message Permission Updated to', + addMessageTop: 'Pinned a Message', + removeMessageTop: 'Removed Pinned Message', + updateTeamMute: 'Updated "Group Mute" to', + onlyTeamOwner: 'Only Group Owner', teamAll: 'Everyone', closeText: 'Close', openText: 'Open', inviteText: 'Invite', - aliasText: 'Remark', - updateAliasSuccessText: 'Remarks modified', - updateAliasFailedText: 'Failed to modify remark', - sendText: 'Send', - noPermission: 'No permission', - callDurationText: 'call duration', - callCancelText: 'canceled', - callRejectedText: 'rejected', - callTimeoutText: 'timeout', - callBusyText: 'busy line', - + aliasText: 'Alias', + updateAliasSuccessText: 'Alias Update Successful', + updateAliasFailedText: 'Alias Update Failed', + sendText: 'Send Message', + noPermission: 'You Do Not Have Permission', + unreadText: 'Unread', + readText: 'Read', + allReadText: 'All Read', + amap: 'Amap', + txmap: 'Tencent Map', + bdmap: 'Baidu Map', + callDurationText: 'Call Duration', + callCancelText: 'Cancelled', + callRejectedText: 'Rejected', + callTimeoutText: 'Timed Out', + callBusyText: 'Recipient Busy', + cancelUploadFailedText: 'Cancel Upload Failed', // conversation-kit - onDismissTeamText: 'Group dismissed', - onRemoveTeamText: 'You were removed', - textMsgText: 'Text message', - audioMsgText: 'Audio message', - videoMsgText: 'Video message', - fileMsgText: 'File message', - callMsgText: 'CDR message', - geoMsgText: 'Location message', - imgMsgText: 'Image message', - notiMsgText: 'Notification', - robotMsgText: 'Chatbot message', - tipMsgText: 'Tip', - unknowMsgText: 'Unknown message', - deleteSessionText: 'Delete', - muteSessionText: 'Enable Do-Not-Disturb', - unmuteSessionText: 'Disable Do-Not-Disturb', + onDismissTeamText: 'Group Disbanded', + onRemoveTeamText: 'You Have Been Removed from the Group', + textMsgText: 'Text Message', + customMsgText: 'Custom Message', + audioMsgText: 'Audio Message', + videoMsgText: 'Video Message', + fileMsgText: 'File Message', + callMsgText: 'Call Log Message', + geoMsgText: 'Location Message', + geoMsgShortText: 'Location', + imgMsgText: 'Image Message', + notiMsgText: 'Notification Message', + robotMsgText: 'Robot Message', + tipMsgText: 'Tip Message', + unknowMsgText: 'Unknown Message', + deleteSessionText: 'Delete Session', + recentConversationText: 'Recent Conversations', + muteSessionText: 'Mute Notifications', + unmuteSessionText: 'Unmute Notifications', deleteStickTopText: 'Unpin', - addStickTopText: 'Pin', - beMentioned: '[You were mentioned]', - + addStickTopText: 'Pin Message', + beMentioned: '[Someone @me]', + aiConversationSelectFailed: 'AI Conversation Selection Failed', + friendText: 'Friend', + aiUserText: 'Digital Person', // contact-kit - teamListTitle: 'Groups', - friendListTitle: 'Friends', - blackListTitle: 'Blocklist', + teamListTitle: 'My Groups', + friendListTitle: 'My Friends', + blackListTitle: 'Blacklist', msgListTitle: 'Message Center', blackListDesc: - '(You will not receive messages from any contacts in the list)', - teamMenuText: 'Groups', - friendMenuText: 'Friends', - blackMenuText: 'Blocklist', + '(You will not receive any messages from contacts in this list)', + aiListTitle: 'My Digital Persons', + teamMenuText: 'My Groups', + friendMenuText: 'My Friends', + blackMenuText: 'Blacklist', + aiMenuText: 'My Digital Persons', msgMenuText: 'Message Center', - acceptedText: 'Request approved', - acceptFailedText: 'Failed to approve the request', - rejectedText: 'Request rejected', - rejectFailedText: 'Failed to reject the request', - getApplyMsgFailedText: 'Failed to get the messsage', - + acceptedText: 'Application Accepted', + acceptFailedText: 'Application Acceptance Failed', + rejectedText: 'Application Rejected', + rejectFailedText: 'Application Rejection Failed', + getApplyMsgFailedText: 'Failed to Retrieve Messages', // search-kit - addFriendText: 'Add friend', - createTeamText: 'Create group', - joinTeamText: 'Join group', - beRemoveTeamText: 'Removed from the group', + addFriendText: 'Add Friend', + createTeamText: 'Create Group', + joinTeamText: 'Join Group', + joinTeamSuccessText: 'Join Group Successful', + joinTeamFailedText: 'Join Group Failed', + beRemoveTeamText: 'Removed from Group', addButtonText: 'Add', - addSuccessText: 'Added', - addFailedText: 'Adding friend failed', + addSuccessText: 'Add Successful', + addFailedText: 'Add Failed', createButtonText: 'Create', - createTeamSuccessText: 'Group created', - createTeamFailedText: 'Failed to create the group', - chatButtonText: 'Chat', - getRelationFailedText: 'Failed to get relationship', - accountNotMatchText: 'No account found', - teamIdNotMatchText: 'No such group number found', + createTeamSuccessText: 'Create Group Successful', + createTeamFailedText: 'Create Group Failed', + chatButtonText: 'Go to Chat', + getRelationFailedText: 'Failed to Get Relationship', + accountNotMatchText: 'Account Not Found', + teamIdNotMatchText: 'Group ID Not Found', searchButtonText: 'Search', - searchTeamPlaceholder: 'Enter a group name', - teamTitle: 'Group name', - teamAvatarText: 'Group avatar', - addTeamMemberText: 'Add member', - searchEmptyText: 'You have not added friends or groups yet', - searchNoResText: 'No search results yet', + searchTeamPlaceholder: 'Enter Group Name', + teamTitle: 'Group Name', + teamAvatarText: 'Group Avatar', + addTeamMemberText: 'Add Member', + searchEmptyText: 'You Have Not Added Any Friends or Groups Yet', + searchNoResText: 'No Results Found', searchFriendTitle: 'Friends', searchTeamTitle: 'Groups', notSupportJoinText: - 'The discussion group cannot be joined directly, please contact the administrator to add you to the group', - + 'Discussion Groups Cannot Be Joined Directly, Please Contact the Administrator to Add You to the Group', // chat-kit - sendToText: 'To', - sendUsageText: - '(Send a message by pressing Enter and break a line using Shift+ Enter)', - sendEmptyText: 'Unable to send an empty message', - teamMutePlaceholder: 'The group owner has muted the group', - enterTeamText: 'joined the group chat', - leaveTeamText: 'left the group chat', - teamMuteText: 'Mute group', - muteAllTeamSuccessText: 'Mute all', - unmuteAllTeamSuccessText: 'Unmute all', - muteAllTeamFailedText: 'Failed to mute all', + sendToText: 'Send to', + topText: 'Pin', + unTopText: 'Unpin', + topFailedText: 'Pin Failed', + collection: 'Collect', + collectionSuccess: 'Collected', + collectionFailed: 'Collect Failed', + getCollectionFailed: 'Failed to Retrieve Collection List', + removeCollectionSuccess: 'Delete Collection Successful', + removeCollectionFailed: 'Delete Collection Failed', + confirmRemoveCollection: 'Confirm Delete Collection?', + sendUsageText: '(Press enter to send, shift+enter to newline)', + sendEmptyText: 'Cannot Send Blank Messages', + teamMutePlaceholder: 'Group Chat is Muted', + enterTeamText: 'Entered the Group', + leaveTeamText: 'Left the Group', + teamMuteText: 'Group Mute', + muteAllTeamSuccessText: 'Enable Mute All Successful', + unmuteAllTeamSuccessText: 'Unmute all members successfully', + muteAllTeamFailedText: 'Failed to mute all members', unmuteAllTeamFailedText: 'Failed to unmute all members', - updateTeamSuccessText: 'Edit succeeded', - updateTeamFailedText: 'Edit failed', - leaveTeamSuccessText: 'You have left the group', + updateTeamSuccessText: 'Update successful', + updateTeamFailedText: 'Update failed', + leaveTeamSuccessText: 'Successfully left the group', leaveTeamFailedText: 'Failed to leave the group', - dismissTeamSuccessText: 'Group dismissed', - dismissTeamFailedText: 'Failed to dismiss the group', - addTeamMemberSuccessText: 'Member added', + dismissTeamSuccessText: 'Group disbanded successfully', + dismissTeamFailedText: 'Failed to disband group', + addTeamMemberSuccessText: 'Member added successfully', addTeamMemberFailedText: 'Failed to add member', - addTeamMemberConfirmText: 'Select group members', - newGroupOwnerText: 'become the new group owner', - removeTeamMemberText: 'Remove', - removeTeamMemberConfirmText: 'Whether to remove the member?', - removeTeamMemberSuccessText: 'Member removed', + addTeamMemberConfirmText: 'Please select the members to add', + removeTeamMemberText: 'Remove member', + removeTeamMemberConfirmText: 'Are you sure you want to remove this member?', + removeTeamMemberSuccessText: 'Member removed successfully', removeTeamMemberFailedText: 'Failed to remove member', - teamTitleConfirmText: 'Group name is required', - teamAvatarConfirmText: 'Group avatar is required', + teamTitleConfirmText: 'Group name cannot be empty', + teamAvatarConfirmText: 'Group avatar cannot be empty', teamIdText: 'Group ID', - teamSignText: 'Introduction', - teamTitlePlaceholder: 'Enter group name', - teamSignPlaceholder: 'Enter a description', + teamSignText: 'Group introduction', + teamTitlePlaceholder: 'Please enter the group name', + teamSignPlaceholder: 'Please enter content', teamOwnerText: 'Group owner', - teamManagerText: 'Admin', - teamManagerLimitText: 'Only admins can edit group information', - teamMemberText: 'Group member', + teamManagerText: 'Group manager', + teamManagerEditText: 'Manage members', + teamManagerEmptyText: 'No group managers yet', + teamOwnerAndManagerText: 'Group owner and managers', + updateTeamManagerSuccessText: 'Group manager updated successfully', + updateTeamManagerFailText: 'Failed to update group manager', + userNotInTeam: 'Member is no longer in the group', + teamMemberNotExist: 'This group chat does not exist', + teamManagerLimitText: 'Who can modify information', + teamInviteModeText: 'Who can invite new members', + teamAtModeText: 'Who can @everyone', + teamTopModeText: 'Who can pin messages', + teamMemberText: 'Group members', teamInfoText: 'Group information', - teamPowerText: 'Permissions', - dismissTeamText: 'Dismiss group', - transferOwnerText: 'transfer group owner', - transferTeamFailedText: 'transfer group owner failed', - transferTeamSuccessText: 'transfer group owner success', - transferOwnerConfirmText: - 'Are you sure you want to transfer the group ownership', - transferToText: 'transfer to', - dismissTeamConfirmText: 'Are you sure you want to dismiss the group', + teamPowerText: 'Group management', + dismissTeamText: 'Disband group', + transferOwnerText: 'Transfer group ownership', + newGroupOwnerText: 'Become the new group owner', + beAddTeamManagersText: 'Appointed as manager', + beRemoveTeamManagersText: 'Removed from manager', + transferTeamFailedText: 'Failed to transfer group ownership', + transferToText: 'Transfer to', + transferTeamSuccessText: 'Group ownership transferred successfully', + transferOwnerConfirmText: 'Are you sure you want to transfer group ownership', + dismissTeamConfirmText: 'Are you sure you want to disband this group', leaveTeamTitle: 'Leave group', - leaveTeamConfirmText: 'Are you sure you want to leave the group', + leaveTeamConfirmText: 'Are you sure you want to leave this group', personUnit: 'person', leaveTeamButtonText: 'Delete and leave', - sendMsgFailedText: 'Failed to send the message', - getHistoryMsgFailedText: 'Failed to get the message history', - sendBlackFailedText: 'You were added to blocklist', - recallMessageText: 'A message was recalled', - reeditText: 'Re-edit', - addChatMemberText: 'Add member', + sendMsgFailedText: 'Message sending failed', + getHistoryMsgFailedText: 'Failed to get history messages', + sendBlackFailedText: 'You have been blacklisted by the other party', + sendNotFriendFailedText: 'Not friends currently', + recallMessageText: 'Recalled a message', + recallReplyMessageText: 'This message has been recalled or deleted', + tipAIMessageText: 'Format not supported', + tipAIFailedMessageText: 'Request to large language model failed', + memberNotExistsText: 'User does not exist', + aiAntiSpamText: 'AI request hit anti-spam', + aiFunctionDisabled: 'AI message function not enabled', + aiMemberBanned: 'User is banned', + aiMemberChatBanned: 'User is muted', + aiFriendNotExists: 'Friend does not exist', + aiMessageHitAntiSpam: 'Message hit anti-spam', + notAnAi: 'Not an AI account', + aiTeamMemberNotExists: 'Group member does not exist', + aiNormalTeamChatBanned: 'Normal group members are muted', + aiTeamChatBanned: 'Group member is muted', + aiBlockFailedText: 'Blocking AI accounts is not allowed', + aiRateLimit: 'Rate limit exceeded', + aiParameterError: 'Parameter error', + reeditText: 'Edit again', + addChatMemberText: 'Add chat member', chatHistoryText: 'Chat history', noMoreText: 'No more messages', - receiveText: 'You have a new message', + noMoreCollectionText: 'Reached the bottom~', + receiveText: 'You have received a new message', strangerNotiText: - 'You are chatting with a non-friend. Be discreet about sharing your personal information.', - teamInviteModeText: 'Only administrators can invite other users', - nickInTeamText: 'Nickname', - editNickInTeamText: 'Edit', - unreadText: 'unread', - readText: 'read', - allReadText: 'All read', - amap: 'Amap', - txmap: 'Tencent map', - bdmap: 'Baidu map', - customMsgText: 'Custom message', - joinTeamSuccessText: 'Join group success', - teamManagerEditText: 'Member management', - teamManagerEmptyText: 'No group management personnel yet', - teamOwnerAndManagerText: 'Group owner and administrator', - updateTeamManagerSuccessText: 'Group administrator modified', - updateTeamManagerFailText: 'Failed to modify group administrator', - userNotInTeam: 'Member is no longer in the group', - teamAtModeText: 'Who can @everyone', - beAddTeamManagersText: 'You were added as an administrator', - beRemoveTeamManagersText: 'You were removed as an administrator', - recallReplyMessageText: 'The message has been recalled or deleted', - updateMyMemberNickSuccess: 'Update my group nickname success', - updateMyMemberNickFailed: 'Update my group nickname failed', - updateBitConfigMaskSuccess: 'Update group message Do-Not-Disturb success', - updateBitConfigMaskFailed: 'Update group message Do-Not-Disturb failed', + 'This person is not your friend, please protect your privacy and safety.', + nickInTeamText: 'My nickname in the group', + editNickInTeamText: 'Edit my nickname in the group', + updateMyMemberNickSuccess: 'Successfully updated my group nickname', + updateMyMemberNickFailed: 'Failed to update my group nickname', + updateBitConfigMaskSuccess: + 'Successfully updated group message Do Not Disturb', + updateBitConfigMaskFailed: 'Failed to update group message Do Not Disturb', + imgText: 'Image', + videoText: 'Video', onlineText: '[Online]', offlineText: '(Offline)', - // emoji 不能随便填,要用固定 key - Laugh: '[Laugh]', - Happy: '[Happy]', - Sexy: '[Sexy]', - Cool: '[Cool]', - Mischievous: '[Mischievous]', - Kiss: '[Kiss]', - Spit: '[Spit]', - Squint: '[Squint]', - Cute: '[Cute]', - Grimace: '[Grimace]', - Snicker: '[Snicker]', - Joy: '[Joy]', - Ecstasy: '[Ecstasy]', - Surprise: '[Surprise]', - Tears: '[Tears]', - Sweat: '[Sweat]', - Angle: '[Angle]', - Funny: '[Funny]', - Awkward: '[Awkward]', - Thrill: '[Thrill]', - Cry: '[Cry]', - Fretting: '[Fretting]', - Terrorist: '[Terrorist]', - Halo: '[Halo]', - Shame: '[Shame]', - Sleep: '[Sleep]', - Tired: '[Tired]', - Mask: '[Mask]', - ok: '[ok]', - AllRight: '[All right]', - Despise: '[Despise]', - Uncomfortable: '[Uncomfortable]', - Disdain: '[Disdain]', - ill: '[ill]', - Mad: '[Mad]', - Ghost: '[Ghost]', - Angry: '[Angry]', - Unhappy: '[Unhappy]', - Frown: '[Frown]', - Broken: '[Broken]', - Beckoning: '[Beckoning]', - Ok: '[Ok]', - Low: '[Low]', - Nice: '[Nice]', - Applause: '[Applause]', - GoodJob: '[Good job]', - Hit: '[Hit]', - Please: '[Please]', - Bye: '[Bye]', - First: '[First]', - Fist: '[Fist]', - GiveMeFive: '[Give me five]', - Knife: '[Knife]', - Hi: '[Hi]', - No: '[No]', - Hold: '[Hold]', - Think: '[Think]', - Pig: '[Pig]', - NoListen: '[No listen]', - NoLook: '[No look]', - NoWords: '[No words]', - Monkey: '[Monkey]', - Bomb: '[Bomb]', - Cloud: '[Cloud]', - Rocket: '[Rocket]', - Ambulance: '[Ambulance]', - Poop: '[Poop]', + pinAIText: 'PIN to top', + aiSearchText: 'AI word search', + aiSearchingText: 'AI searching...', + aiProxyFailedText: 'Model request exception', + aiSearchInputPlaceholder: 'Enter more information', + aiTranslateText: 'AI processing', + aiTranslatingText: 'AI processing...', + aiTranslatedText: 'Using', + aiTranslatePlaceholder: 'Translate to...', + aiTranslateEmptyText: 'Please enter the content to translate', + aiSendingText: 'Large model request in response', + searchTipText: 'Press Enter to search', } -const emojiLocaleConfig = {} - export default LocaleConfig diff --git a/react/src/components/IMApp/locales/zh.ts b/react/src/components/IMApp/locales/zh.ts index 83c6d1d..70b63f8 100644 --- a/react/src/components/IMApp/locales/zh.ts +++ b/react/src/components/IMApp/locales/zh.ts @@ -13,7 +13,9 @@ const LocaleConfig = { deleteText: '删除', recallText: '撤回', forwardText: '转发', + forwardSuccessText: '转发成功', forwardFailedText: '转发失败', + recentForwardText: '最近转发', sendBtnText: '发送', replyText: '回复', commentText: '留言', @@ -32,10 +34,9 @@ const LocaleConfig = { removeBlackFailedText: '解除拉黑失败', maxSelectedText: '最多只能选择', selectedText: '已选', - friendsText: '位好友', strangerText: '位陌生人', emailErrorText: '邮箱格式不正确', - uploadLimitText: '图片或文件大小最大支持', + uploadLimitText: '图片视频或文件大小最大支持', uploadLimitUnit: 'M', uploadImgFailedText: '上传图片失败', accountText: '账号', @@ -67,6 +68,7 @@ const LocaleConfig = { applyFriendText: '添加您为好友', acceptResultText: '已同意', rejectResultText: '已拒绝', + expiredResultText: '已过期', beRejectResultText: '拒绝了好友申请', passResultText: '通过了好友申请', rejectTeamInviteText: '拒绝了群邀请', @@ -76,6 +78,9 @@ const LocaleConfig = { updateTeamInviteMode: '更新了群权限“邀请他人权限”为', updateTeamUpdateTeamMode: '更新了群权限“群资料修改权限”为', updateAllowAt: '更新了“@所有人权限”为', + updateAllowTop: '置顶消息权限更新为', + addMessageTop: '置顶了一条消息', + removeMessageTop: '移除了置顶消息', updateTeamMute: '更新了“群禁言”为', onlyTeamOwner: '仅群主', teamAll: '所有人', @@ -98,6 +103,7 @@ const LocaleConfig = { callRejectedText: '已拒绝', callTimeoutText: '已超时', callBusyText: '对方忙', + cancelUploadFailedText: '取消上传失败', // conversation-kit onDismissTeamText: '群已被解散', onRemoveTeamText: '您已被移出群组', @@ -108,17 +114,22 @@ const LocaleConfig = { fileMsgText: '文件消息', callMsgText: '话单消息', geoMsgText: '地理位置消息', + geoMsgShortText: '位置', imgMsgText: '图片消息', notiMsgText: '通知消息', robotMsgText: '机器消息', tipMsgText: '提示消息', unknowMsgText: '未知消息', deleteSessionText: '删除会话', + recentConversationText: '最近会话', muteSessionText: '开启免打扰', unmuteSessionText: '取消免打扰', deleteStickTopText: '取消置顶', addStickTopText: '置顶消息', beMentioned: '[有人@我]', + aiConversationSelectFailed: '选择 AI 会话失败', + friendText: '好友', + aiUserText: '数字人', // contact-kit teamListTitle: '我的群组', @@ -126,9 +137,11 @@ const LocaleConfig = { blackListTitle: '黑名单', msgListTitle: '消息中心', blackListDesc: '(你不会收到列表中任何联系人的消息)', + aiListTitle: '我的数字人', teamMenuText: '我的群组', friendMenuText: '我的好友', blackMenuText: '黑名单', + aiMenuText: '我的数字人', msgMenuText: '消息中心', acceptedText: '已同意该申请', acceptFailedText: '同意该申请失败', @@ -141,6 +154,7 @@ const LocaleConfig = { createTeamText: '创建群组', joinTeamText: '加入群组', joinTeamSuccessText: '加入群组成功', + joinTeamFailedText: '加入群组失败', beRemoveTeamText: '被移出群组', addButtonText: '添加', addSuccessText: '添加成功', @@ -165,6 +179,16 @@ const LocaleConfig = { // chat-kit sendToText: '发送给', + topText: '置顶', + unTopText: '取消置顶', + topFailedText: '置顶失败', + collection: '收藏', + collectionSuccess: '已收藏', + collectionFailed: '收藏失败', + getCollectionFailed: '查询收藏列表失败', + removeCollectionSuccess: '删除收藏成功', + removeCollectionFailed: '删除收藏失败', + confirmRemoveCollection: '确认删除收藏?', sendUsageText: '(按enter直接发送,shift+enter换行)', sendEmptyText: '不能发送空白消息', teamMutePlaceholder: '当前群聊禁言中', @@ -202,9 +226,11 @@ const LocaleConfig = { updateTeamManagerSuccessText: '修改群管理员成功', updateTeamManagerFailText: '修改群管理员失败', userNotInTeam: '成员已不在群中', + teamMemberNotExist: '该群聊已不存在', teamManagerLimitText: '谁可以修改资料', teamInviteModeText: '谁可以邀请新成员', teamAtModeText: '谁可以@所有人', + teamTopModeText: '谁可以置顶消息', teamMemberText: '群成员', teamInfoText: '群资料', teamPowerText: '群管理', @@ -225,12 +251,30 @@ const LocaleConfig = { sendMsgFailedText: '消息发送失败', getHistoryMsgFailedText: '获取历史消息失败', sendBlackFailedText: '您已被对方拉入黑名单', + sendNotFriendFailedText: '当前非好友关系', recallMessageText: '撤回了一条消息', recallReplyMessageText: '该消息已撤回或删除', + tipAIMessageText: '格式不支持', + tipAIFailedMessageText: '请求大语言模型失败', + memberNotExistsText: '用户不存在', + aiAntiSpamText: 'AI 请求命中反垃圾', + aiFunctionDisabled: 'AI 消息功能未开通', + aiMemberBanned: '用户被禁用', + aiMemberChatBanned: '用户被禁言', + aiFriendNotExists: '好友不存在', + aiMessageHitAntiSpam: '消息命中反垃圾', + notAnAi: '不是数字人账号', + aiTeamMemberNotExists: '群成员不存在', + aiNormalTeamChatBanned: '群普通成员禁言', + aiTeamChatBanned: '群成员被禁言', + aiBlockFailedText: '不允许对数字人进行黑名单操作', + aiRateLimit: '频率超限', + aiParameterError: '参数错误', reeditText: '重新编辑', addChatMemberText: '添加聊天成员', chatHistoryText: '聊天记录', noMoreText: '没有更多消息了', + noMoreCollectionText: '已滑到最底部~', receiveText: '您收到了新消息', strangerNotiText: '当前不是您的好友,请注意保护个人隐私安全。', nickInTeamText: '我在群里的昵称', @@ -239,77 +283,22 @@ const LocaleConfig = { updateMyMemberNickFailed: '更新我的群昵称失败', updateBitConfigMaskSuccess: '更新群消息免打扰成功', updateBitConfigMaskFailed: '更新群消息免打扰失败', + imgText: '图片', + videoText: '视频', onlineText: '[在线]', offlineText: '(离线)', - - // emoji 不能随便填,要用固定 key,,参考 demo - Laugh: '[大笑]', - Happy: '[开心]', - Sexy: '[色]', - Cool: '[酷]', - Mischievous: '[奸笑]', - Kiss: '[亲]', - Spit: '[伸舌头]', - Squint: '[眯眼]', - Cute: '[可爱]', - Grimace: '[鬼脸]', - Snicker: '[偷笑]', - Joy: '[喜悦]', - Ecstasy: '[狂喜]', - Surprise: '[惊讶]', - Tears: '[流泪]', - Sweat: '[流汗]', - Angle: '[天使]', - Funny: '[笑哭]', - Awkward: '[尴尬]', - Thrill: '[惊恐]', - Cry: '[大哭]', - Fretting: '[烦躁]', - Terrorist: '[恐怖]', - Halo: '[两眼冒星]', - Shame: '[害羞]', - Sleep: '[睡着]', - Tired: '[冒星]', - Mask: '[口罩]', - ok: '[OK]', - AllRight: '[好吧]', - Despise: '[鄙视]', - Uncomfortable: '[难受]', - Disdain: '[不屑]', - ill: '[不舒服]', - Mad: '[愤怒]', - Ghost: '[鬼怪]', - Angry: '[发怒]', - Unhappy: '[不高兴]', - Frown: '[皱眉]', - Broken: '[心碎]', - Beckoning: '[心动]', - Ok: '[好的]', - Low: '[低级]', - Nice: '[赞]', - Applause: '[鼓掌]', - GoodJob: '[给力]', - Hit: '[打你]', - Please: '[阿弥陀佛]', - Bye: '[拜拜]', - First: '[第一]', - Fist: '[拳头]', - GiveMeFive: '[手掌]', - Knife: '[剪刀]', - Hi: '[招手]', - No: '[不要]', - Hold: '[举着]', - Think: '[思考]', - Pig: '[猪头]', - NoListen: '[不听]', - NoLook: '[不看]', - NoWords: '[不说]', - Monkey: '[猴子]', - Bomb: '[炸弹]', - Cloud: '[筋斗云]', - Rocket: '[火箭]', - Ambulance: '[救护车]', - Poop: '[便便]', + pinAIText: 'PIN 置顶', + aiSearchText: 'AI 划词搜', + aiSearchingText: 'AI 划词搜索中...', + aiProxyFailedText: '模型请求异常', + aiSearchInputPlaceholder: '补充输入更多信息', + aiTranslateText: 'AI 处理', + aiTranslatingText: 'AI 处理中...', + aiTranslatedText: '使用', + aiTranslatePlaceholder: '翻译为...', + aiTranslateEmptyText: '请输入需要翻译的内容', + aiSendingText: '大模型请求响应中', + searchTipText: 'Enter 搜索', } export default LocaleConfig