1、腾讯IM模块格式化

This commit is contained in:
cuiyouliang 2025-06-27 09:53:36 +08:00
parent 99dfe74b26
commit d77452dc25
206 changed files with 4258 additions and 7118 deletions

View File

@ -3,10 +3,7 @@ import { TUIGlobal } from '@tencentcloud/universal-api';
let vueVersion: number;
let framework = 'vue2';
let createVNode = (
arg1: any,
arg2: any,
): { component: any; props: any; data: any } => {
let createVNode = (arg1: any, arg2: any): { component: any; props: any; data: any } => {
return {} as { component: any; props: any; data: any };
};
let render = (arg1: any, arg2: any) => {
@ -14,17 +11,11 @@ let render = (arg1: any, arg2: any) => {
};
try {
if (
(Vue as any)?.default?.version
&& (Vue as any)?.default?.version?.startsWith('2.7.')
) {
if ((Vue as any)?.default?.version && (Vue as any)?.default?.version?.startsWith('2.7.')) {
// >= Vue 2.7.0
vueVersion = 2.7;
TUIGlobal.Vue = (Vue as any)?.getCurrentInstance()?.appContext?.app;
} else if (
(Vue as any)?.default?.version
&& (Vue as any)?.default?.version?.startsWith('2.')
) {
} else if ((Vue as any)?.default?.version && (Vue as any)?.default?.version?.startsWith('2.')) {
// < Vue 2.7.0
vueVersion = 2;
TUIGlobal.Vue = (Vue as any).default;

View File

@ -8,10 +8,7 @@
<Icon :file="backSVG" />
</div>
<div class="chat-header-container">
<div
v-if="isNotRoomChat"
:class="['chat-header-content', !isPC && 'chat-header-h5-content']"
>
<div v-if="isNotRoomChat" :class="['chat-header-content', !isPC && 'chat-header-h5-content']">
{{ currentConversationName }}
</div>
<div>
@ -19,11 +16,7 @@
</div>
</div>
<div :class="['chat-header-setting', !isPC && 'chat-header-h5-setting']">
<div
v-for="(item, index) in props.headerExtensionList"
:key="index"
@click.stop="handleExtensions(item)"
>
<div v-for="(item, index) in props.headerExtensionList" :key="index" @click.stop="handleExtensions(item)">
<Icon :file="item.icon" />
</div>
</div>
@ -31,12 +24,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, withDefaults } from '../../../adapter-vue';
import {
TUIStore,
StoreName,
TUITranslateService,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, TUITranslateService, IConversationModel } from '@tencentcloud/chat-uikit-engine';
import { TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core';
// import { JoinGroupCard } from '@tencentcloud/call-uikit-vue';
import Icon from '../../common/Icon.vue';
@ -49,8 +37,8 @@ const props = withDefaults(
headerExtensionList: ExtensionInfo[];
}>(),
{
headerExtensionList: () => ([]),
},
headerExtensionList: () => []
}
);
const emits = defineEmits(['closeChat']);
@ -62,21 +50,21 @@ const isNotRoomChat = ref<boolean>(TUIChatConfig.getChatType() !== TUIConstants.
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
TUIStore.watch(StoreName.CHAT, {
typingStatus: onTypingStatusUpdated,
typingStatus: onTypingStatusUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
TUIStore.unwatch(StoreName.CHAT, {
typingStatus: onTypingStatusUpdated,
typingStatus: onTypingStatusUpdated
});
});
@ -102,7 +90,6 @@ function onTypingStatusUpdated(status: boolean) {
currentConversationName.value = currentConversation.value?.getShowName() || '';
}
}
</script>
<style lang="scss" scoped>
.chat-header {
@ -152,7 +139,7 @@ function onTypingStatusUpdated(status: boolean) {
align-items: center;
}
&-content{
&-content {
margin: 0 20px;
text-align: center;
}

View File

@ -26,7 +26,7 @@ class TUIChatConfig {
InputQuickReplies: true,
InputMention: true,
MessageSearch: true,
ReadStatus: true,
ReadStatus: true
};
this.theme = 'light';
}
@ -76,8 +76,6 @@ class TUIChatConfig {
const ChatConfig = TUIChatConfig.getInstance();
const hideTUIChatFeatures = ChatConfig.hideTUIChatFeatures.bind(ChatConfig);
export {
hideTUIChatFeatures,
};
export { hideTUIChatFeatures };
export default ChatConfig;

View File

@ -1,15 +1,15 @@
import { IEmojiGroupList } from '../../../interface';
/**
* Custom big emoji
*/
* Custom big emoji
*/
export const CUSTOM_BIG_EMOJI_URL: string = '';
export const CUSTOM_BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [];
/**
* Custom basic emoji
*/
* Custom basic emoji
*/
export const CUSTOM_BASIC_EMOJI_URL: string = '';
export const CUSTOM_BASIC_EMOJI_URL_MAPPING: Record<string, string> = {};

View File

@ -77,7 +77,7 @@ export const DEFAULT_BASIC_EMOJI_URL_MAPPING: Record<string, string> = {
'[TUIEmoji_Prohibit]': 'emoji_58@2x.png',
'[TUIEmoji_Convinced]': 'emoji_59@2x.png',
'[TUIEmoji_Knife]': 'emoji_60@2x.png',
'[TUIEmoji_Like]': 'emoji_61@2x.png',
'[TUIEmoji_Like]': 'emoji_61@2x.png'
};
export const BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [
@ -85,30 +85,60 @@ export const BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [
emojiGroupID: 1,
type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL,
list: ['yz00', 'yz01', 'yz02', 'yz03', 'yz04', 'yz05', 'yz06', 'yz07', 'yz08',
'yz09', 'yz10', 'yz11', 'yz12', 'yz13', 'yz14', 'yz15', 'yz16', 'yz17'],
list: [
'yz00',
'yz01',
'yz02',
'yz03',
'yz04',
'yz05',
'yz06',
'yz07',
'yz08',
'yz09',
'yz10',
'yz11',
'yz12',
'yz13',
'yz14',
'yz15',
'yz16',
'yz17'
]
},
{
emojiGroupID: 2,
type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL,
list: ['ys00', 'ys01', 'ys02', 'ys03', 'ys04', 'ys05', 'ys06', 'ys07', 'ys08',
'ys09', 'ys10', 'ys11', 'ys12', 'ys13', 'ys14', 'ys15'],
list: ['ys00', 'ys01', 'ys02', 'ys03', 'ys04', 'ys05', 'ys06', 'ys07', 'ys08', 'ys09', 'ys10', 'ys11', 'ys12', 'ys13', 'ys14', 'ys15']
},
{
emojiGroupID: 3,
type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL,
list: ['gcs00', 'gcs01', 'gcs02', 'gcs03', 'gcs04', 'gcs05', 'gcs06', 'gcs07',
'gcs08', 'gcs09', 'gcs10', 'gcs11', 'gcs12', 'gcs13', 'gcs14', 'gcs15', 'gcs16'],
},
list: [
'gcs00',
'gcs01',
'gcs02',
'gcs03',
'gcs04',
'gcs05',
'gcs06',
'gcs07',
'gcs08',
'gcs09',
'gcs10',
'gcs11',
'gcs12',
'gcs13',
'gcs14',
'gcs15',
'gcs16'
]
}
];
export const BASIC_EMOJI_NAME_TO_KEY_MAPPING = {
...Object.fromEntries(
Object.entries(emojiCNLocales)?.map(([key, val]) => [val, key]),
),
...Object.fromEntries(
Object.entries(emojiENLocales)?.map(([key, val]) => [val, key]),
),
...Object.fromEntries(Object.entries(emojiCNLocales)?.map(([key, val]) => [val, key])),
...Object.fromEntries(Object.entries(emojiENLocales)?.map(([key, val]) => [val, key]))
};

View File

@ -1,6 +1,12 @@
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { CUSTOM_BASIC_EMOJI_URL, CUSTOM_BIG_EMOJI_URL, CUSTOM_BASIC_EMOJI_URL_MAPPING, CUSTOM_BIG_EMOJI_GROUP_LIST } from './custom-emoji';
import { DEFAULT_BASIC_EMOJI_URL, BIG_EMOJI_GROUP_LIST, DEFAULT_BASIC_EMOJI_URL_MAPPING, BASIC_EMOJI_NAME_TO_KEY_MAPPING, DEFAULT_BIG_EMOJI_URL } from './default-emoji';
import {
DEFAULT_BASIC_EMOJI_URL,
BIG_EMOJI_GROUP_LIST,
DEFAULT_BASIC_EMOJI_URL_MAPPING,
BASIC_EMOJI_NAME_TO_KEY_MAPPING,
DEFAULT_BIG_EMOJI_URL
} from './default-emoji';
import { default as emojiCNLocales } from './locales/zh_cn';
import { IEmojiGroupList } from '../../../interface';
import { EMOJI_TYPE } from '../../../constant';
@ -17,10 +23,10 @@ const EMOJI_GROUP_LIST: IEmojiGroupList = [
emojiGroupID: 0,
type: EMOJI_TYPE.BASIC,
url: BASIC_EMOJI_URL,
list: Object.keys(BASIC_EMOJI_URL_MAPPING),
list: Object.keys(BASIC_EMOJI_URL_MAPPING)
},
...BIG_EMOJI_GROUP_LIST,
...CUSTOM_BIG_EMOJI_GROUP_LIST,
...CUSTOM_BIG_EMOJI_GROUP_LIST
];
/**
@ -49,7 +55,7 @@ const transformTextWithKeysToEmojiNames = (text: string): string => {
const reg = /(\[.+?\])/g;
let txt: string = text;
if (reg.test(text)) {
txt = text.replace(reg, match => BASIC_EMOJI_URL_MAPPING[match] ? convertKeyToEmojiName(match) : match);
txt = text.replace(reg, (match) => (BASIC_EMOJI_URL_MAPPING[match] ? convertKeyToEmojiName(match) : match));
}
return txt;
};
@ -68,20 +74,20 @@ const transformTextWithEmojiNamesToKeys = (text: string) => {
const reg = /(\[.+?\])/g;
let txt: string = text;
if (reg.test(text)) {
txt = text.replace(reg, match => BASIC_EMOJI_NAME_TO_KEY_MAPPING[match] || match);
txt = text.replace(reg, (match) => BASIC_EMOJI_NAME_TO_KEY_MAPPING[match] || match);
}
return txt;
};
/**
* The configuration aims to provide compatibility with versions prior to 2.2.0
*/
* The configuration aims to provide compatibility with versions prior to 2.2.0
*/
const emojiConfig = {
emojiBaseUrl: BASIC_EMOJI_URL,
emojiUrlMapping: BASIC_EMOJI_URL_MAPPING,
emojiNameMapping: {
...emojiCNLocales,
},
...emojiCNLocales
}
};
/**
@ -136,5 +142,5 @@ export {
parseTextToRenderArray,
transformTextWithKeysToEmojiNames,
transformTextWithEmojiNamesToKeys,
emojiConfig,
emojiConfig
};

View File

@ -60,7 +60,7 @@ const Emoji = {
'[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[Knife]',
'[TUIEmoji_Like]': '[Like]',
'[TUIEmoji_Like]': '[Like]'
};
export default Emoji;

View File

@ -60,7 +60,7 @@ const Emoji: Record<string, string> = {
'[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[刀]',
'[TUIEmoji_Like]': '[赞]',
'[TUIEmoji_Like]': '[赞]'
};
export default Emoji;

View File

@ -60,7 +60,7 @@ const Emoji: Record<string, string> = {
'[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[刀]',
'[TUIEmoji_Like]': '[讚]',
'[TUIEmoji_Like]': '[讚]'
};
export default Emoji;

View File

@ -1,8 +1,5 @@
<template>
<Overlay
:visible="isShowForwardPanel"
:useMask="false"
>
<Overlay :visible="isShowForwardPanel" :useMask="false">
<Transfer
:title="TUITranslateService.t('TUIChat.转发')"
:isSearch="false"
@ -17,12 +14,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from '../../../adapter-vue';
import TUIChatEngine, {
TUIStore,
StoreName,
TUIChatService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUIChatService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import Overlay from '../../common/Overlay/index.vue';
import Transfer from '../../common/Transfer/index.vue';
import { Toast, TOAST_TYPE } from '../../../components/common/Toast';
@ -45,14 +37,14 @@ const customConversationList = ref();
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
singleForwardMessageID: onSingleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
singleForwardMessageID: onSingleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated
});
// tuistore data must be cleared when closing the forward panel
@ -72,10 +64,7 @@ function onMultipleForwardMessageIDUpdated(params: { isMergeForward: boolean; me
return;
}
isMergeForward = false;
const {
isMergeForward: _isMergeForward,
messageIDList: selectedMessageIDList,
} = params || {};
const { isMergeForward: _isMergeForward, messageIDList: selectedMessageIDList } = params || {};
if (selectedMessageIDList?.length > 0) {
isMergeForward = _isMergeForward;
selectedToForwardMessageIDList = selectedMessageIDList;
@ -83,7 +72,7 @@ function onMultipleForwardMessageIDUpdated(params: { isMergeForward: boolean; me
} else {
Toast({
message: TUITranslateService.t('TUIChat.未选择消息'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
}
@ -107,35 +96,29 @@ function openForwardPanel(): void {
function finishSelected(selectedConvIDWrapperList: Array<{ userID: string }>): void {
if (selectedConvIDWrapperList?.length === 0) return;
// to reuse Transfer, so we have to get conversationModel by userID instead of ConversationID
const selectedConversationList = selectedConvIDWrapperList.map(IDWrapper => TUIStore.getConversationModel(IDWrapper.userID));
const unsentMessageQueue = selectedToForwardMessageIDList
.map(messageID => TUIStore.getMessageModel(messageID))
.sort((a, b) => a.time - b.time);
const selectedConversationList = selectedConvIDWrapperList.map((IDWrapper) => TUIStore.getConversationModel(IDWrapper.userID));
const unsentMessageQueue = selectedToForwardMessageIDList.map((messageID) => TUIStore.getMessageModel(messageID)).sort((a, b) => a.time - b.time);
const forwardPromises = selectedConversationList.map((conversation) => {
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation,
messageType: TUIChatEngine.TYPES.MSG_MERGER,
messageType: TUIChatEngine.TYPES.MSG_MERGER
};
return TUIChatService.sendForwardMessage(
[conversation],
unsentMessageQueue,
{
return TUIChatService.sendForwardMessage([conversation], unsentMessageQueue, {
needMerge: isMergeForward,
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
params: {
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
},
},
);
needReadReceipt: isEnabledMessageReadReceiptGlobal()
}
});
});
Promise.allSettled(forwardPromises).then((results) => {
for (const result of results) {
const { status } = result;
if (status === 'rejected') {
const errorMessage = result.reason.code === 80001 ? TUITranslateService.t('TUIChat.内容包含敏感词汇') : result.reason.message as string;
const errorMessage = result.reason.code === 80001 ? TUITranslateService.t('TUIChat.内容包含敏感词汇') : (result.reason.message as string);
Toast({
message: errorMessage,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
break;
}
@ -152,7 +135,7 @@ function getTransforRenderDataList() {
// To achieve reusability of Transfer, userID is used here instead of ConversationID
userID: conversation.conversationID,
nick: conversation.getShowName(),
avatar: conversation.getAvatar(),
avatar: conversation.getAvatar()
};
});
}

View File

@ -6,75 +6,31 @@
'emoji-picker-h5': !isPC
}"
>
<ul
ref="emojiPickerListRef"
:class="['emoji-picker-list', !isPC && 'emoji-picker-h5-list']"
>
<ul ref="emojiPickerListRef" :class="['emoji-picker-list', !isPC && 'emoji-picker-h5-list']">
<li
v-for="(childrenItem, childrenIndex) in currentEmojiList"
:key="childrenIndex"
class="emoji-picker-list-item"
@click="select(childrenItem, childrenIndex)"
>
<img
v-if="currentTabItem.type === EMOJI_TYPE.BASIC"
class="emoji"
:src="currentTabItem.url + BASIC_EMOJI_URL_MAPPING[childrenItem]"
>
<img
v-else-if="currentTabItem.type === EMOJI_TYPE.BIG"
class="emoji-big"
:src="currentTabItem.url + childrenItem + '@2x.png'"
>
<img
v-else
class="emoji-custom emoji-big"
:src="currentTabItem.url + childrenItem"
>
<img v-if="currentTabItem.type === EMOJI_TYPE.BASIC" class="emoji" :src="currentTabItem.url + BASIC_EMOJI_URL_MAPPING[childrenItem]" />
<img v-else-if="currentTabItem.type === EMOJI_TYPE.BIG" class="emoji-big" :src="currentTabItem.url + childrenItem + '@2x.png'" />
<img v-else class="emoji-custom emoji-big" :src="currentTabItem.url + childrenItem" />
</li>
</ul>
<ul class="emoji-picker-tab">
<li
v-for="(item, index) in list"
:key="index"
class="emoji-picker-tab-item"
@click="toggleEmojiTab(index)"
>
<Icon
v-if="item.type === EMOJI_TYPE.BASIC"
class="icon"
:file="faceIcon"
/>
<img
v-else-if="item.type === EMOJI_TYPE.BIG"
class="icon-big"
:src="item.url + item.list[0] + '@2x.png'"
>
<img
v-else
class="icon-custom icon-big"
:src="item.url + item.list[0]"
>
</li>
<li
v-if="isUniFrameWork"
class="send-btn"
@click="sendMessage"
>
发送
<li v-for="(item, index) in list" :key="index" class="emoji-picker-tab-item" @click="toggleEmojiTab(index)">
<Icon v-if="item.type === EMOJI_TYPE.BASIC" class="icon" :file="faceIcon" />
<img v-else-if="item.type === EMOJI_TYPE.BIG" class="icon-big" :src="item.url + item.list[0] + '@2x.png'" />
<img v-else class="icon-custom icon-big" :src="item.url + item.list[0]" />
</li>
<li v-if="isUniFrameWork" class="send-btn" @click="sendMessage">发送</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import {
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
} from '@tencentcloud/chat-uikit-engine';
import { TUIChatService, TUIStore, StoreName, IConversationModel, SendMessageParams } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue';
import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg';
@ -98,13 +54,13 @@ const currentEmojiList = ref<string[]>(list?.value[0]?.list);
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
currentConversation: onCurrentConversationUpdate
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
currentConversation: onCurrentConversationUpdate
});
});
@ -121,7 +77,7 @@ const toggleEmojiTab = (index: number) => {
const select = (item: any, index: number) => {
const options: any = {
emoji: { key: item, name: convertKeyToEmojiName(item) },
type: currentTabItem?.value?.type,
type: currentTabItem?.value?.type
};
switch (currentTabItem?.value?.type) {
case EMOJI_TYPE.BASIC:
@ -146,15 +102,13 @@ const select = (item: any, index: number) => {
const sendFaceMessage = (index: number, listItem: IEmojiGroup) => {
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
index: listItem.emojiGroupID,
data: listItem.list[index],
data: listItem.list[index]
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams;
TUIChatService.sendFaceMessage(options);
};

View File

@ -1,24 +1,10 @@
<template>
<ToolbarItemContainer
ref="container"
:iconFile="faceIcon"
title="表情"
@onDialogShow="onDialogShow"
@onDialogClose="onDialogClose"
>
<EmojiPickerDialog
@insertEmoji="insertEmoji"
@sendMessage="sendMessage"
@onClose="onClose"
/>
<ToolbarItemContainer ref="container" :iconFile="faceIcon" title="表情" @onDialogShow="onDialogShow" @onDialogClose="onDialogClose">
<EmojiPickerDialog @insertEmoji="insertEmoji" @sendMessage="sendMessage" @onClose="onClose" />
</ToolbarItemContainer>
</template>
<script lang="ts" setup>
import {
TUIStore,
StoreName,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IConversationModel } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg';
@ -45,7 +31,7 @@ const container = ref<InstanceType<typeof ToolbarItemContainer>>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const onDialogShow = (dialogRef: any) => {
@ -75,7 +61,7 @@ const onClose = () => {
};
defineExpose({
closeEmojiPicker: onClose,
closeEmojiPicker: onClose
});
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -11,83 +11,39 @@
>
<div :class="['evaluate', !isPC && 'evaluate-h5']">
<div :class="['evaluate-header', !isPC && 'evaluate-h5-header']">
<div
:class="[
'evaluate-header-content',
!isPC && 'evaluate-h5-header-content',
]"
>
{{ TUITranslateService.t("Evaluate.请对本次服务进行评价") }}
<div :class="['evaluate-header-content', !isPC && 'evaluate-h5-header-content']">
{{ TUITranslateService.t('Evaluate.请对本次服务进行评价') }}
</div>
<div
v-if="!isPC"
:class="[
'evaluate-header-close',
!isPC && 'evaluate-h5-header-close',
]"
@click.stop="closeDialog"
>
{{ TUITranslateService.t("关闭") }}
<div v-if="!isPC" :class="['evaluate-header-close', !isPC && 'evaluate-h5-header-close']" @click.stop="closeDialog">
{{ TUITranslateService.t('关闭') }}
</div>
</div>
<div :class="['evaluate-content', !isPC && 'evaluate-h5-content']">
<ul
:class="[
'evaluate-content-list',
!isPC && 'evaluate-h5-content-list',
]"
>
<ul :class="['evaluate-content-list', !isPC && 'evaluate-h5-content-list']">
<li
v-for="(item, index) in starList"
:key="index"
:class="[
'evaluate-content-list-item',
!isPC && 'evaluate-h5-content-list-item',
]"
:class="['evaluate-content-list-item', !isPC && 'evaluate-h5-content-list-item']"
@click.stop="selectStar(index)"
>
<Icon
v-if="index <= currentStarIndex"
:file="starLightIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
<Icon
v-else
:file="starIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
<Icon v-if="index <= currentStarIndex" :file="starLightIcon" :width="isPC ? '20px' : '30px'" :height="isPC ? '20px' : '30px'" />
<Icon v-else :file="starIcon" :width="isPC ? '20px' : '30px'" :height="isPC ? '20px' : '30px'" />
</li>
</ul>
<textarea
v-model="comment"
:class="[
'evaluate-content-text',
!isPC && 'evaluate-h5-content-text',
]"
/>
<div
:class="[
'evaluate-content-button',
!isPC && 'evaluate-h5-content-button',
]"
>
<button
:class="['btn', isEvaluateValid ? 'btn-valid' : 'btn-invalid']"
@click="submitEvaluate"
>
{{ TUITranslateService.t("Evaluate.提交评价") }}
<textarea v-model="comment" :class="['evaluate-content-text', !isPC && 'evaluate-h5-content-text']" />
<div :class="['evaluate-content-button', !isPC && 'evaluate-h5-content-button']">
<button :class="['btn', isEvaluateValid ? 'btn-valid' : 'btn-invalid']" @click="submitEvaluate">
{{ TUITranslateService.t('Evaluate.提交评价') }}
</button>
</div>
</div>
<div :class="['evaluate-adv', !isPC && 'evaluate-h5-adv']">
{{ TUITranslateService.t("Evaluate.服务评价工具") }}
{{ "(" + TUITranslateService.t("Evaluate.使用") }}
{{ TUITranslateService.t('Evaluate.服务评价工具') }}
{{ '(' + TUITranslateService.t('Evaluate.使用') }}
<a @click="openLink(Link.customMessage)">
{{ TUITranslateService.t(`Evaluate.${Link.customMessage.label}`) }}
</a>
{{ TUITranslateService.t("Evaluate.搭建") + ")" }}
{{ TUITranslateService.t('Evaluate.搭建') + ')' }}
</div>
</div>
</ToolbarItemContainer>
@ -100,7 +56,7 @@ import TUIChatEngine, {
IConversationModel,
TUIChatService,
SendMessageParams,
SendMessageOptions,
SendMessageOptions
} from '@tencentcloud/chat-uikit-engine';
import { ref, computed } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
@ -120,8 +76,8 @@ const evaluateIcon = TUIChatConfig.getTheme() === 'dark' ? evaluateIconDark : ev
const props = defineProps({
starTotal: {
type: Number,
default: 5,
},
default: 5
}
});
const emits = defineEmits(['onDialogPopupShowOrHide']);
@ -135,7 +91,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const isEvaluateValid = computed(() => comment.value.length || currentStarIndex.value >= 0);
@ -182,26 +138,24 @@ const submitEvaluate = () => {
businessID: CHAT_MSG_CUSTOM_TYPE.EVALUATE,
version: 1,
score: currentStarIndex.value + 1,
comment: comment.value,
comment: comment.value
}),
description: '对本次的服务评价',
extension: '对本次的服务评价',
extension: '对本次的服务评价'
};
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload,
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
};
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_CUSTOM,
messageType: TUIChatEngine.TYPES.MSG_CUSTOM
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
};
TUIChatService.sendCustomMessage(options as SendMessageParams, sendMessageOptions);
// close dialog after submit evaluate

View File

@ -8,14 +8,7 @@
@onIconClick="onIconClick"
>
<div :class="['file-upload', !isPC && 'file-upload-h5']">
<input
ref="inputRef"
title="文件"
type="file"
data-type="file"
accept="*"
@change="sendFileMessage"
>
<input ref="inputRef" title="文件" type="file" data-type="file" accept="*" @change="sendFileMessage" />
</div>
</ToolbarItemContainer>
</template>
@ -26,7 +19,7 @@ import TUIChatEngine, {
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
SendMessageOptions
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
@ -44,7 +37,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const onIconClick = () => {
@ -60,27 +53,25 @@ const sendFileMessage = (e: any) => {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file: e?.target,
file: e?.target
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_FILE,
messageType: TUIChatEngine.TYPES.MSG_FILE
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
};
TUIChatService.sendFileMessage(options, sendMessageOptions);
e.target.value = '';
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
</style>

View File

@ -7,10 +7,7 @@
:needDialog="false"
@onIconClick="onIconClick"
>
<div
v-if="!isUniFrameWork"
:class="['image-upload', !isPC && 'image-upload-h5']"
>
<div v-if="!isUniFrameWork" :class="['image-upload', !isPC && 'image-upload-h5']">
<input
ref="inputRef"
title="图片"
@ -18,7 +15,7 @@
data-type="image"
accept="image/gif,image/jpeg,image/jpg,image/png,image/bmp,image/webp"
@change="sendImageInWeb"
>
/>
</div>
</ToolbarItemContainer>
</template>
@ -29,7 +26,7 @@ import TUIChatEngine, {
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
SendMessageOptions
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed } from '../../../../adapter-vue';
@ -49,8 +46,8 @@ const props = defineProps({
// camera: Take a photo using the camera
imageSourceType: {
type: String,
default: 'album',
},
default: 'album'
}
});
const inputRef = ref();
@ -59,29 +56,27 @@ const theme = TUIChatConfig.getTheme();
const IMAGE_TOOLBAR_SHOW_MAP = {
web_album: {
icon: theme === 'dark' ? imageIconDark : imageIconLight,
title: '图片',
title: '图片'
},
uni_album: {
icon: imageUniIcon,
title: '图片',
title: '图片'
},
uni_camera: {
icon: cameraUniIcon,
title: '拍照',
},
title: '拍照'
}
};
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const imageToolbarForShow = computed((): { icon: string; title: string } => {
if (isUniFrameWork) {
return props.imageSourceType === 'camera'
? IMAGE_TOOLBAR_SHOW_MAP['uni_camera']
: IMAGE_TOOLBAR_SHOW_MAP['uni_album'];
return props.imageSourceType === 'camera' ? IMAGE_TOOLBAR_SHOW_MAP['uni_camera'] : IMAGE_TOOLBAR_SHOW_MAP['uni_album'];
} else {
return IMAGE_TOOLBAR_SHOW_MAP['web_album'];
}
@ -98,7 +93,7 @@ const onIconClick = () => {
sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res: any) {
sendImageMessage(res);
},
}
});
} else {
// uni-app H5/App send image
@ -107,7 +102,7 @@ const onIconClick = () => {
sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res) {
sendImageMessage(res);
},
}
});
}
} else {
@ -130,27 +125,25 @@ const sendImageMessage = (files: any) => {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file: files,
file: files
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_IMAGE,
messageType: TUIChatEngine.TYPES.MSG_IMAGE
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
};
TUIChatService.sendImageMessage(options, sendMessageOptions);
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
</style>

View File

@ -1,18 +1,6 @@
<template>
<div
:class="[
'message-input-toolbar',
!isPC && 'message-input-toolbar-h5',
isUniFrameWork && 'message-input-toolbar-uni',
]"
>
<div
:class="[
'message-input-toolbar-list',
!isPC && 'message-input-toolbar-h5-list',
isUniFrameWork && 'message-input-toolbar-uni-list',
]"
>
<div :class="['message-input-toolbar', !isPC && 'message-input-toolbar-h5', isUniFrameWork && 'message-input-toolbar-uni']">
<div :class="['message-input-toolbar-list', !isPC && 'message-input-toolbar-h5-list', isUniFrameWork && 'message-input-toolbar-uni-list']">
<EmojiPicker
v-if="isRenderedEmojiPicker"
ref="emojiPickerRef"
@ -21,15 +9,9 @@
@dialogCloseInH5="dialogCloseInH5"
@changeToolbarDisplayType="(type) => emits('changeToolbarDisplayType', type)"
/>
<ImageUpload
v-if="featureConfig.InputImage"
imageSourceType="album"
/>
<ImageUpload v-if="featureConfig.InputImage" imageSourceType="album" />
<FileUpload v-if="featureConfig.InputFile" />
<VideoUpload
v-if="featureConfig.InputVideo"
videoSourceType="album"
/>
<VideoUpload v-if="featureConfig.InputVideo" videoSourceType="album" />
<Evaluate v-if="featureConfig.InputEvaluation" />
<Words v-if="featureConfig.InputQuickReplies" />
<template v-if="extensionListShowInStart[0]">
@ -45,10 +27,7 @@
/>
</template>
</div>
<div
v-if="extensionListShowInEnd[0] && isPC"
:class="['message-input-toolbar-list-end']"
>
<div v-if="extensionListShowInEnd[0] && isPC" :class="['message-input-toolbar-list-end']">
<ToolbarItemContainer
v-for="(extension, index) in extensionListShowInEnd"
:key="index"
@ -68,21 +47,12 @@
@submit="onUserSelectorSubmit"
@cancel="onUserSelectorCancel"
/>
<div
v-if="isH5"
ref="h5Dialog"
:class="['message-input-toolbar-h5-dialog']"
/>
<div v-if="isH5" ref="h5Dialog" :class="['message-input-toolbar-h5-dialog']" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue';
import TUIChatEngine, {
IConversationModel,
TUIStore,
StoreName,
TUIReportService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { IConversationModel, TUIStore, StoreName, TUIReportService } from '@tencentcloud/chat-uikit-engine';
import TUICore, { ExtensionInfo, TUIConstants } from '@tencentcloud/tui-core';
import EmojiPicker from './emoji-picker/index.vue';
import ImageUpload from './image-upload/index.vue';
@ -103,10 +73,10 @@ interface IProps {
interface IEmits {
(e: 'scrollToLatestMessage'): void;
(e: 'changeToolbarDisplayType', type: ToolbarDisplayType): void;
(e: 'insertEmoji', emoji: any): void
(e: 'insertEmoji', emoji: any): void;
}
const props = withDefaults(defineProps<IProps>(), {
displayType: 'none',
displayType: 'none'
});
const emits = defineEmits<IEmits>();
@ -124,23 +94,26 @@ isRenderedEmojiPicker.value = featureConfig.InputEmoji || featureConfig.InputSti
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate,
activeConversation: onActiveConversationUpdate
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate,
activeConversation: onActiveConversationUpdate
});
});
watch(() => props.displayType, (newValue) => {
watch(
() => props.displayType,
(newValue) => {
if (newValue === 'none') {
emojiPickerRef.value?.closeEmojiPicker();
} else {
emits('scrollToLatestMessage');
}
});
}
);
const onActiveConversationUpdate = (conversationID: string) => {
if (!conversationID) {
@ -162,25 +135,25 @@ const getExtensionList = () => {
params.filterVideo = true;
enableSampleTaskStatus('customerService');
}
currentExtensionList.value = [
...TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, params),
].filter((extension: ExtensionInfo) => {
currentExtensionList.value = [...TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, params)].filter(
(extension: ExtensionInfo) => {
if (extension?.data?.name === 'search') {
return featureConfig.MessageSearch;
}
return true;
});
}
);
reportExtension(currentExtensionList.value);
};
function reportExtension(extensionList:ExtensionInfo[]){
extensionList.forEach((extension: ExtensionInfo)=>{
function reportExtension(extensionList: ExtensionInfo[]) {
extensionList.forEach((extension: ExtensionInfo) => {
const _name = extension?.data?.name;
if(_name === 'voiceCall'){
if (_name === 'voiceCall') {
TUIReportService.reportFeature(203, 'voice-call');
} else if (_name === 'videoCall') {
TUIReportService.reportFeature(203, 'video-call');
} else if(_name === 'quickRoom'){
} else if (_name === 'quickRoom') {
TUIReportService.reportFeature(204);
}
});
@ -196,7 +169,7 @@ const extensionListShowInStart = computed<ExtensionInfo[]>(() => {
const extensionListShowInEnd = computed<ExtensionInfo[]>(() => {
if (isPC) {
const searchExtension = currentExtensionList.value.find(extension => extension?.data?.name === 'search');
const searchExtension = currentExtensionList.value.find((extension) => extension?.data?.name === 'search');
return searchExtension ? [searchExtension] : [];
}
return [];
@ -222,7 +195,7 @@ const onCallExtensionClicked = (extension: ExtensionInfo, callType: number) => {
if (currentConversation?.value?.type === TUIChatEngine.TYPES.CONV_C2C) {
extension.listener?.onClicked?.({
userIDList: [currentConversation?.value?.conversationID?.slice(3)],
type: callType,
type: callType
});
} else if (isGroup.value) {
currentUserSelectorExtension.value = extension;
@ -268,7 +241,7 @@ const dialogCloseInH5 = (dialogDom: any) => {
};
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.message-input-toolbar {
border-top: 1px solid #f4f5f9;

View File

@ -1,30 +1,9 @@
<template>
<div
ref="toolbarItemRef"
:class="[
'toolbar-item-container',
!isPC && 'toolbar-item-container-h5',
isUniFrameWork && 'toolbar-item-container-uni',
]"
>
<div
:class="[
'toolbar-item-container-icon',
isUniFrameWork && 'toolbar-item-container-uni-icon',
]"
@click="toggleToolbarItem"
>
<Icon
:file="props.iconFile"
class="icon"
:width="props.iconWidth"
:height="props.iconHeight"
/>
<div ref="toolbarItemRef" :class="['toolbar-item-container', !isPC && 'toolbar-item-container-h5', isUniFrameWork && 'toolbar-item-container-uni']">
<div :class="['toolbar-item-container-icon', isUniFrameWork && 'toolbar-item-container-uni-icon']" @click="toggleToolbarItem">
<Icon :file="props.iconFile" class="icon" :width="props.iconWidth" :height="props.iconHeight" />
</div>
<div
v-if="isUniFrameWork"
:class="['toolbar-item-container-uni-title']"
>
<div v-if="isUniFrameWork" :class="['toolbar-item-container-uni-title']">
{{ props.title }}
</div>
<div
@ -34,7 +13,7 @@
'toolbar-item-container-dialog',
isDark && 'toolbar-item-container-dialog-dark',
!isPC && 'toolbar-item-container-h5-dialog',
isUniFrameWork && 'toolbar-item-container-uni-dialog',
isUniFrameWork && 'toolbar-item-container-uni-dialog'
]"
>
<BottomPopup
@ -61,30 +40,30 @@ import TUIChatConfig from '../../config';
const props = defineProps({
iconFile: {
type: String,
required: true,
required: true
},
title: {
type: String,
default: '',
default: ''
},
needDialog: {
type: Boolean,
default: true,
default: true
},
iconWidth: {
type: String,
default: '20px',
default: '20px'
},
iconHeight: {
type: String,
default: '20px',
default: '20px'
},
// Whether to display the bottom popup dialog on mobile devices
// Invalid on PC
needBottomPopup: {
type: Boolean,
default: false,
},
default: false
}
});
const emits = defineEmits(['onIconClick', 'onDialogClose', 'onDialogShow']);
@ -99,7 +78,7 @@ const toggleToolbarItem = () => {
if (isPC) {
outsideClick.listen({
domRefs: toolbarItemRef.value,
handler: closeToolbarItem,
handler: closeToolbarItem
});
}
if (!props.needDialog) {
@ -132,7 +111,7 @@ const onPopupClose = () => {
};
defineExpose({
toggleDialogDisplay,
toggleDialogDisplay
});
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -1,12 +1,5 @@
<template>
<Dialog
:show="show"
:isH5="!isPC"
:isHeaderShow="false"
:isFooterShow="false"
:background="false"
@update:show="toggleShow"
>
<Dialog :show="show" :isH5="!isPC" :isHeaderShow="false" :isFooterShow="false" :background="false" @update:show="toggleShow">
<Transfer
:isSearch="true"
:title="title"
@ -20,10 +13,7 @@
</Dialog>
</template>
<script setup lang="ts">
import {
TUIGroupService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGroupService, TUIUserService } from '@tencentcloud/chat-uikit-engine';
import { ref, computed, watch } from '../../../../adapter-vue';
import Dialog from '../../../common/Dialog/index.vue';
import Transfer from '../../../common/Transfer/index.vue';
@ -33,16 +23,16 @@ const props = defineProps({
// type: voiceCall/groupCall/...
type: {
type: String,
default: '',
default: ''
},
currentConversation: {
type: Object,
default: () => ({}),
default: () => ({})
},
isGroup: {
type: Boolean,
default: false,
},
default: false
}
});
const emits = defineEmits(['submit', 'cancel']);
const show = ref<boolean>(false);
@ -52,7 +42,7 @@ const searchMemberList = ref<any[]>([]);
const selfUserID = ref<string>('');
const titleMap: any = {
voiceCall: '发起群语音',
videoCall: '发起群视频',
videoCall: '发起群视频'
};
const title = computed(() => {
return titleMap[props.type] ? titleMap[props.type] : '';
@ -71,11 +61,9 @@ watch(
if (props.isGroup && show.value) {
groupID.value = props.currentConversation.groupProfile.groupID;
TUIGroupService.getGroupMemberList({
groupID: groupID.value,
groupID: groupID.value
}).then((res: any) => {
memberList.value = res?.data?.memberList?.filter(
(user: any) => user?.userID !== selfUserID.value,
);
memberList.value = res?.data?.memberList?.filter((user: any) => user?.userID !== selfUserID.value);
searchMemberList.value = memberList.value;
});
} else {
@ -86,14 +74,12 @@ watch(
}
},
{
immediate: true,
},
immediate: true
}
);
const search = (searchInfo: string) => {
const results = memberList.value?.filter(
(member: any) => member?.userID === searchInfo,
);
const results = memberList.value?.filter((member: any) => member?.userID === searchInfo);
searchMemberList.value = results?.length ? results : memberList.value;
};
@ -122,6 +108,6 @@ const toggleShow = (showStatus: boolean) => {
};
defineExpose({
toggleShow,
toggleShow
});
</script>

View File

@ -4,23 +4,11 @@
:title="handleTitle()"
:needDialog="false"
:iconWidth="isUniFrameWork ? '32px' : '20px'"
:iconHeight="isUniFrameWork
? props.videoSourceType === 'album'
? '20px'
: '25px'
: '18px'
"
:iconHeight="isUniFrameWork ? (props.videoSourceType === 'album' ? '20px' : '25px') : '18px'"
@onIconClick="onIconClick"
>
<div :class="['video-upload', !isPC && 'video-upload-h5']">
<input
ref="inputRef"
title="视频"
type="file"
data-type="video"
accept="video/*"
@change="sendVideoInWeb"
>
<input ref="inputRef" title="视频" type="file" data-type="video" accept="video/*" @change="sendVideoInWeb" />
</div>
</ToolbarItemContainer>
</template>
@ -31,7 +19,7 @@ import TUIChatEngine, {
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
SendMessageOptions
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref } from '../../../../adapter-vue';
@ -51,8 +39,8 @@ const props = defineProps({
// camera: Take a video using the camera
videoSourceType: {
type: String,
default: 'album',
},
default: 'album'
}
});
const inputRef = ref();
@ -61,7 +49,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const handleIcon = (): string => {
@ -99,7 +87,7 @@ const onIconClick = () => {
maxDuration: 60,
success: function (res: any) {
sendVideoMessage(res);
},
}
});
} else {
TUIGlobal?.chooseVideo({
@ -108,7 +96,7 @@ const onIconClick = () => {
compressed: false,
success: function (res: any) {
sendVideoMessage(res);
},
}
});
}
} else {
@ -129,27 +117,25 @@ const sendVideoMessage = (file: any) => {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file,
file
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_VIDEO,
messageType: TUIChatEngine.TYPES.MSG_VIDEO
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
};
TUIChatService.sendVideoMessage(options, sendMessageOptions);
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
</style>

View File

@ -1,2 +1,2 @@
import Words from "./index.vue";
import Words from './index.vue';
export default Words;

View File

@ -12,23 +12,12 @@
<div :class="['words', !isPC && 'words-h5']">
<div :class="['words-header', !isPC && 'words-h5-header']">
<span :class="['words-header-title', !isPC && 'words-h5-header-title']">
{{ TUITranslateService.t("Words.常用语-快捷回复工具") }}
</span>
<span
v-if="!isPC"
:class="['words-header-close', !isPC && 'words-h5-header-close']"
@click="closeDialog"
>
关闭
{{ TUITranslateService.t('Words.常用语-快捷回复工具') }}
</span>
<span v-if="!isPC" :class="['words-header-close', !isPC && 'words-h5-header-close']" @click="closeDialog"> 关闭 </span>
</div>
<ul :class="['words-list', !isPC && 'words-h5-list']">
<li
v-for="(item, index) in wordsList"
:key="index"
:class="['words-list-item', !isPC && 'words-h5-list-item']"
@click="selectWord(item)"
>
<li v-for="(item, index) in wordsList" :key="index" :class="['words-list-item', !isPC && 'words-h5-list-item']" @click="selectWord(item)">
{{ TUITranslateService.t(`Words.${item.value}`) }}
</li>
</ul>
@ -36,14 +25,7 @@
</ToolbarItemContainer>
</template>
<script setup lang="ts">
import {
TUITranslateService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, TUIStore, StoreName, IConversationModel, SendMessageParams, TUIChatService } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import wordsIconLight from '../../../../assets/icon/words-light.svg';
@ -61,19 +43,17 @@ const container = ref();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const selectWord = (item: any) => {
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
text: TUITranslateService.t(`Words.${item.value}`),
text: TUITranslateService.t(`Words.${item.value}`)
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams;
TUIChatService.sendTextMessage(options);
// close dialog after submit evaluate

View File

@ -14,27 +14,15 @@
@onTyping="onTyping"
@onAt="onAt"
/>
<MessageInputButton
v-if="!props.isMuted"
@sendMessage="sendMessage"
/>
<MessageInputAt
v-if="props.enableAt"
ref="messageInputAtRef"
@insertAt="insertAt"
@onAtListOpen="onAtListOpen"
/>
<MessageInputButton v-if="!props.isMuted" @sendMessage="sendMessage" />
<MessageInputAt v-if="props.enableAt" ref="messageInputAtRef" @insertAt="insertAt" @onAtListOpen="onAtListOpen" />
</div>
<MessageInputQuote />
</div>
</template>
<script setup lang="ts">
import {
TUIStore,
StoreName,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IConversationModel } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../adapter-vue';
import MessageInputEditor from './message-input-editor.vue';
import MessageInputAt from './message-input-at/index.vue';
@ -47,32 +35,32 @@ import { isPC, isH5 } from '../../../utils/env';
const props = defineProps({
placeholder: {
type: String,
default: 'this is placeholder',
default: 'this is placeholder'
},
isMuted: {
type: Boolean,
default: true,
default: true
},
muteText: {
type: String,
default: '',
default: ''
},
enableInput: {
type: Boolean,
default: true,
default: true
},
enableAt: {
type: Boolean,
default: true,
default: true
},
enableDragUpload: {
type: Boolean,
default: true,
default: true
},
enableTyping: {
type: Boolean,
default: true,
},
default: true
}
});
const emit = defineEmits(['sendMessage', 'resetReplyOrReference', 'onTyping']);
@ -83,7 +71,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
}
});
const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
@ -103,10 +91,7 @@ const sendMessage = async () => {
}
return editor;
});
await sendMessages(
editorContentList,
currentConversation.value,
);
await sendMessages(editorContentList, currentConversation.value);
emit('sendMessage');
editor.value?.resetEditor();
};
@ -132,12 +117,12 @@ const reEdit = (content: any) => {
defineExpose({
insertEmoji,
reEdit,
reEdit
});
</script>
<style scoped lang="scss">
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.message-input-wrapper {
box-sizing: border-box;

View File

@ -1,8 +1,5 @@
import { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
import TUIChatEngine, {
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { isH5 } from '../../../../utils/env';
@ -13,17 +10,13 @@ let showMemberList: any[] = [];
let selectedIndex: number = 0;
let isGroup = false;
let command:
| ((props: {
id?: string | undefined;
userID?: string | undefined;
isAll?: boolean | undefined;
}) => void)
| ((props: { id?: string | undefined; userID?: string | undefined; isAll?: boolean | undefined }) => void)
| ((arg0: { id: any; label: any }) => any);
const all = {
userID: TUIChatEngine.TYPES.MSG_AT_ALL,
nick: '所有人',
isAll: true,
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg',
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg'
};
TUIStore.watch(StoreName.CONV, {
@ -40,7 +33,7 @@ TUIStore.watch(StoreName.CONV, {
showMemberList = [];
}
}
},
}
});
TUIStore.watch(StoreName.CUSTOM, {
@ -50,7 +43,7 @@ TUIStore.watch(StoreName.CUSTOM, {
allMemberList = [all, ...memberList];
showMemberList = allMemberList;
}
},
}
});
const MessageInputAtSuggestion = () => {
@ -62,8 +55,7 @@ const MessageInputAtSuggestion = () => {
}
const queryResult = allMemberList?.filter(
(item: { nick: string; userID: string }) =>
item?.nick?.toLowerCase()?.startsWith(props?.query?.toLowerCase())
|| item?.userID?.toLowerCase()?.startsWith(props?.query?.toLowerCase()),
item?.nick?.toLowerCase()?.startsWith(props?.query?.toLowerCase()) || item?.userID?.toLowerCase()?.startsWith(props?.query?.toLowerCase())
);
showMemberList = queryResult?.length ? queryResult : allMemberList;
TUIGlobal.setShowMemberList(showMemberList);
@ -76,7 +68,7 @@ const MessageInputAtSuggestion = () => {
id?: string;
userID?: string;
isAll?: boolean;
}>,
}>
) => {
if (!isGroup) {
return;
@ -89,7 +81,7 @@ const MessageInputAtSuggestion = () => {
if (rect?.left && rect?.top && !isH5) {
TUIGlobal.handleAtListPosition({
left: rect?.left,
top: rect?.top,
top: rect?.top
});
}
command = props.command;
@ -106,7 +98,7 @@ const MessageInputAtSuggestion = () => {
if (rect?.left && rect?.top && !isH5) {
TUIGlobal.handleAtListPosition({
left: rect?.left,
top: rect?.top,
top: rect?.top
});
}
},
@ -147,18 +139,17 @@ const MessageInputAtSuggestion = () => {
showMemberList = allMemberList;
TUIGlobal.handleAtListPosition({
left: 0,
top: 0,
top: 0
});
},
}
};
},
}
};
};
const upHandler = () => {
if (!showMemberList?.length) return;
selectedIndex
= (selectedIndex + showMemberList?.length - 1) % showMemberList?.length;
selectedIndex = (selectedIndex + showMemberList?.length - 1) % showMemberList?.length;
TUIGlobal.setCurrentSelectIndex(selectedIndex);
};
@ -176,10 +167,10 @@ const selectItem = (index: number) => {
if (!showMemberList?.length) return;
const item = showMemberList[index];
if (item) {
command
&& command({
command &&
command({
id: (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID
});
}
};

View File

@ -1,23 +1,9 @@
<template>
<BottomPopup
:show="showAtList"
@onClose="closeAt"
>
<div
ref="MessageInputAt"
:class="[isPC ? 'message-input-at' : 'message-input-at-h5']"
>
<div
ref="dialog"
class="member-list"
>
<header
v-if="!isPC"
class="member-list-title"
>
<span class="title">{{
TUITranslateService.t("TUIChat.选择提醒的人")
}}</span>
<BottomPopup :show="showAtList" @onClose="closeAt">
<div ref="MessageInputAt" :class="[isPC ? 'message-input-at' : 'message-input-at-h5']">
<div ref="dialog" class="member-list">
<header v-if="!isPC" class="member-list-title">
<span class="title">{{ TUITranslateService.t('TUIChat.选择提醒的人') }}</span>
</header>
<ul class="member-list-box">
<li
@ -28,10 +14,7 @@
:class="[index === selectedIndex && 'selected']"
@click="selectItem(index)"
>
<img
class="member-list-box-body-avatar"
:src="handleMemberAvatar(item)"
>
<img class="member-list-box-body-avatar" :src="handleMemberAvatar(item)" />
<span class="member-list-box-body-name">
{{ handleMemberName(item) }}
</span>
@ -42,12 +25,7 @@
</BottomPopup>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIStore,
StoreName,
TUIGroupService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUIGroupService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, watch } from '../../../../adapter-vue';
import { isPC, isH5 } from '../../../../utils/env';
@ -65,7 +43,7 @@ const showMemberList = ref<Array<any>>();
const isGroup = ref(false);
const position = ref({
left: 0,
top: 0,
top: 0
});
const selectedIndex = ref(0);
const currentConversationID = ref('');
@ -74,7 +52,7 @@ const all = {
userID: TUIChatEngine.TYPES.MSG_AT_ALL,
nick: '所有人',
isAll: true,
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg',
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg'
};
TUIStore.watch(StoreName.CONV, {
@ -94,7 +72,7 @@ TUIStore.watch(StoreName.CONV, {
TUIGroupService.switchGroup('');
}
}
},
}
});
TUIStore.watch(StoreName.GRP, {
@ -103,7 +81,7 @@ TUIStore.watch(StoreName.GRP, {
allMemberList.value = [all, ...memberList.value];
showMemberList.value = allMemberList.value;
TUIStore.update(StoreName.CUSTOM, 'memberList', memberList.value);
},
}
});
const toggleAtList = (show: boolean) => {
@ -132,7 +110,7 @@ TUIGlobal.setCurrentSelectIndex = setCurrentSelectIndex;
TUIGlobal.setShowMemberList = setShowMemberList;
defineExpose({
toggleAtList,
toggleAtList
});
watch(
@ -142,9 +120,8 @@ watch(
return;
}
MessageInputAt.value.style.left = position.value.left + 'px';
MessageInputAt.value.style.top
= position.value.top - MessageInputAt.value.clientHeight + 'px';
},
MessageInputAt.value.style.top = position.value.top - MessageInputAt.value.clientHeight + 'px';
}
);
const closeAt = () => {
@ -152,7 +129,7 @@ const closeAt = () => {
showMemberList.value = allMemberList.value;
position.value = {
left: 0,
top: 0,
top: 0
};
};
@ -164,7 +141,7 @@ const selectItem = (index: number) => {
const item = showMemberList?.value[index];
emits('insertAt', {
id: (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID
});
}
}
@ -172,10 +149,7 @@ const selectItem = (index: number) => {
};
const handleMemberAvatar = (item: any) => {
return (
(item as any)?.avatar
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
);
return (item as any)?.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
};
const handleMemberName = (item: any) => {
@ -183,7 +157,7 @@ const handleMemberName = (item: any) => {
};
</script>
<style scoped lang="scss">
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-input-at {
position: fixed;
@ -191,7 +165,7 @@ const handleMemberName = (item: any) => {
max-height: 10rem;
overflow: hidden auto;
background: #fff;
box-shadow: 0 0.06rem 0.63rem 0 rgba(2,16,43,0.15);
box-shadow: 0 0.06rem 0.63rem 0 rgba(2, 16, 43, 0.15);
border-radius: 0.13rem;
}
@ -202,7 +176,7 @@ const handleMemberName = (item: any) => {
cursor: pointer;
&:hover {
background: rgba(0,110,255,0.1);
background: rgba(0, 110, 255, 0.1);
}
}
@ -223,7 +197,7 @@ const handleMemberName = (item: any) => {
.selected,
&:hover {
background: rgba(0,110,255,0.1);
background: rgba(0, 110, 255, 0.1);
}
&-name {
@ -242,7 +216,7 @@ const handleMemberName = (item: any) => {
}
.selected {
background: rgba(0,110,255,0.1);
background: rgba(0, 110, 255, 0.1);
}
}

View File

@ -1,19 +1,10 @@
<template>
<div :class="['message-input-button', !isPC && 'message-input-button-h5']">
<button
v-if="props.enableSend"
class="message-input-button-cont"
data-type="text"
:disabled="false"
@click="sendMessage"
>
<p
v-if="displayHover"
class="message-input-button-hover"
>
{{ TUITranslateService.t("TUIChat.按Enter发送Ctrl+Enter换行") }}
<button v-if="props.enableSend" class="message-input-button-cont" data-type="text" :disabled="false" @click="sendMessage">
<p v-if="displayHover" class="message-input-button-hover">
{{ TUITranslateService.t('TUIChat.按Enter发送Ctrl+Enter换行') }}
</p>
{{ TUITranslateService.t("发送") }}
{{ TUITranslateService.t('发送') }}
</button>
</div>
</template>
@ -27,8 +18,8 @@ import TUIChatConfig from '../config';
const props = defineProps({
enableSend: {
type: Boolean,
default: true,
},
default: true
}
});
const displayHover = ref(TUIChatConfig.getChatType() !== TUIConstants.TUIChat.TYPE.ROOM);
@ -40,7 +31,7 @@ const sendMessage = () => {
};
</script>
<style scoped lang="scss">
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.message-input-button {
position: absolute;
@ -84,7 +75,7 @@ const sendMessage = () => {
opacity: 0.3;
&::before {
content: "";
content: '';
position: absolute;
width: 0;
height: 0;

View File

@ -1,11 +1,6 @@
<template>
<div
:class="['message-input-editor-container', isH5 && 'message-input-editor-container-h5']"
>
<div
v-if="isMuted"
class="message-input-mute"
>
<div :class="['message-input-editor-container', isH5 && 'message-input-editor-container-h5']">
<div v-if="isMuted" class="message-input-mute">
{{ muteText }}
</div>
<div
@ -24,11 +19,7 @@
</template>
<script setup lang="ts">
import { toRefs, ref, onMounted, watch, onUnmounted } from '../../../adapter-vue';
import {
TUIStore,
StoreName,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IMessageModel } from '@tencentcloud/chat-uikit-engine';
import { Editor, JSONContent, Extension } from '@tiptap/core';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
@ -47,36 +38,36 @@ import DraftManager from '../utils/conversationDraft';
const props = defineProps({
placeholder: {
type: String,
default: 'this is placeholder',
default: 'this is placeholder'
},
replayOrReferenceMessage: {
type: Object,
default: () => ({}),
default: () => ({})
},
isMuted: {
type: Boolean,
default: true,
default: true
},
muteText: {
type: String,
default: '',
default: ''
},
enableInput: {
type: Boolean,
default: true,
default: true
},
enableAt: {
type: Boolean,
default: true,
default: true
},
enableDragUpload: {
type: Boolean,
default: true,
default: true
},
enableTyping: {
type: Boolean,
default: true,
},
default: true
}
});
const emits = defineEmits(['sendMessage', 'onTyping', 'onAt']);
@ -92,9 +83,9 @@ const fileMap = new Map<string, any>();
const DisableDefaultEnter = Extension.create({
addKeyboardShortcuts() {
return {
Enter: () => true,
Enter: () => true
};
},
}
});
function onCurrentConversationIDUpdated(conversationID: string) {
@ -104,7 +95,7 @@ function onCurrentConversationIDUpdated(conversationID: string) {
currentConversationID.value,
getEditorHTML(),
DraftManager.generateAbstract(getEditorContent()),
currentQuoteMessage.value,
currentQuoteMessage.value
);
}
resetEditor();
@ -131,18 +122,18 @@ onMounted(() => {
DisableDefaultEnter,
Placeholder.configure({
emptyEditorClass: 'is-editor-empty',
placeholder: placeholder.value,
placeholder: placeholder.value
}),
Mention.configure({
HTMLAttributes: {
class: 'mention',
class: 'mention'
},
suggestion: enableAt.value && (MessageInputAtSuggestion() as any),
suggestion: enableAt.value && (MessageInputAtSuggestion() as any)
}),
CustomImage.configure({
inline: true,
allowBase64: true,
}),
allowBase64: true
})
],
autofocus: !isH5,
editable: true,
@ -151,7 +142,7 @@ onMounted(() => {
handlePaste() {
// prevent editor's default paste for resolve emoji & marked down line break
return true;
},
}
},
// handle input editor typing (only in C2C and enable typing)
onUpdate({ editor, transaction }) {
@ -167,12 +158,8 @@ onMounted(() => {
if (isH5 && document?.getElementById('app')?.style) {
// set app height when keyboard popup
const keyboardHeight = document.body.scrollHeight - window.innerHeight;
(
document.getElementById('app') as any
).style.marginBottom = `${keyboardHeight}px`;
(
document.getElementById('app') as any
).style.height = `calc(100% - ${keyboardHeight}px)`;
(document.getElementById('app') as any).style.marginBottom = `${keyboardHeight}px`;
(document.getElementById('app') as any).style.height = `calc(100% - ${keyboardHeight}px)`;
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
@ -185,31 +172,31 @@ onMounted(() => {
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
},
}
})
: null;
if (isH5) {
const targetBottomDom = document.querySelector('.message-input-toolbar') as HTMLElement || editorDom.value;
const targetBottomDom = (document.querySelector('.message-input-toolbar') as HTMLElement) || editorDom.value;
riseInput(editorDom.value, targetBottomDom);
}
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
currentConversationID: onCurrentConversationIDUpdated
});
TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
quoteMessage: onQuoteMessageUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
currentConversationID: onCurrentConversationIDUpdated
});
TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
quoteMessage: onQuoteMessageUpdated
});
// clear map store
@ -289,25 +276,19 @@ async function handleFileDropOrPaste(e: any, type: string) {
if (!enableDragUpload.value && type === 'drop') {
return;
}
if (
(type === 'drop' && e.dataTransfer)
|| (type === 'paste' && e.clipboardData)
) {
const files
= type === 'drop' ? e?.dataTransfer?.files : e?.clipboardData?.files;
if ((type === 'drop' && e.dataTransfer) || (type === 'paste' && e.clipboardData)) {
const files = type === 'drop' ? e?.dataTransfer?.files : e?.clipboardData?.files;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const isImage = file.type.startsWith('image/');
const fileSrc = isImage
? URL.createObjectURL(file)
: await drawFileCanvasToImageUrl(file);
const fileSrc = isImage ? URL.createObjectURL(file) : await drawFileCanvasToImageUrl(file);
editor?.commands?.insertContent({
type: 'custom-image',
attrs: {
src: fileSrc,
alt: file?.name,
class: isImage ? 'normal' : 'file',
},
class: isImage ? 'normal' : 'file'
}
});
fileMap.set(fileSrc, file);
if (i === files.length - 1) {
@ -324,7 +305,7 @@ async function handleFileDropOrPaste(e: any, type: string) {
* create file icon image
* To avoid creating img dom with the same icon repeatedly, record the previous type of icon that has been created.
* The format of the record is map<icon type, img dom>.
*/
*/
const fileIconDomMap = new Map<string, HTMLImageElement>();
const addImageProcess = (src: string, type: string) => {
return new Promise((resolve, reject) => {
@ -375,18 +356,7 @@ const drawFileCanvasToImageUrl = async (file: any) => {
const handleFileIconForShow = (type: string) => {
const urlBase = 'https://web.sdk.qcloud.com/component/TUIKit/assets/file-';
const fileTypes = [
'image',
'pdf',
'text',
'ppt',
'presentation',
'sheet',
'zip',
'word',
'video',
'unknown',
];
const fileTypes = ['image', 'pdf', 'text', 'ppt', 'presentation', 'sheet', 'zip', 'word', 'video', 'unknown'];
let url = '';
let iconType = '';
fileTypes.forEach((typeName: string) => {
@ -397,7 +367,7 @@ const handleFileIconForShow = (type: string) => {
});
return {
iconSrc: url ? url : urlBase + 'unknown.svg',
iconType: iconType ? iconType : 'unknown',
iconType: iconType ? iconType : 'unknown'
};
};
@ -431,16 +401,13 @@ function parsePCEditorContent(): ITipTapEditorContent[] {
const content: ITipTapEditorContent[] = [];
handleEditorContent(editorJSON, content);
if (
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1].type === 'text'
&& content[content.length - 1].payload?.text?.endsWith('\n')
content.length > 0 &&
content[content.length - 1] &&
content[content.length - 1].type === 'text' &&
content[content.length - 1].payload?.text?.endsWith('\n')
) {
const text = content[content.length - 1].payload.text || '';
content[content.length - 1].payload.text = text?.substring(
0,
text.lastIndexOf('\n'),
);
content[content.length - 1].payload.text = text?.substring(0, text.lastIndexOf('\n'));
}
return content;
}
@ -449,11 +416,7 @@ function handleEditorContent(root: JSONContent, content: ITipTapEditorContent[])
if (!root || !root.type) {
return;
}
if (
root.type !== 'text'
&& root.type !== 'custom-image'
&& root.type !== 'mention'
) {
if (root.type !== 'text' && root.type !== 'custom-image' && root.type !== 'mention') {
if (root.type === 'paragraph' || root.type === 'hardBreak') {
handleEditorNode(root, content);
}
@ -471,69 +434,49 @@ function handleEditorContent(root: JSONContent, content: ITipTapEditorContent[])
function handleEditorNode(node: JSONContent, content: ITipTapEditorContent[]) {
// handle enter
if (node.type === 'hardBreak') {
if (content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content[content.length - 1].payload.text += '\n';
} else {
content.push({
type: 'text',
payload: { text: '\n' },
payload: { text: '\n' }
});
}
} else if (node.type === 'paragraph') {
if (
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content[content.length - 1].payload.text += '\n';
}
} else if (
node.type === 'text'
|| (node.type === 'custom-image' && node?.attrs?.class?.includes('emoji'))
) {
} else if (node.type === 'text' || (node.type === 'custom-image' && node?.attrs?.class?.includes('emoji'))) {
// Process text and emojis
const text = node.type === 'text' ? node?.text : node?.attrs?.alt;
if (
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content[content.length - 1].payload.text += text;
} else {
content.push({
type: 'text',
payload: { text: text },
payload: { text: text }
});
}
} else if (
node.type === 'custom-image' && node?.attrs?.class?.includes('normal')
) {
} else if (node.type === 'custom-image' && node?.attrs?.class?.includes('normal')) {
// Process rich text images
content.push({
type: 'image',
payload: { file: fileMap?.get(node?.attrs?.src) },
payload: { file: fileMap?.get(node?.attrs?.src) }
});
} else if (node.type === 'custom-image' && node?.attrs?.class?.includes('file')) {
const file = fileMap?.get(node?.attrs?.src);
content.push({
type: file?.type?.includes('video') ? 'video' : 'file',
payload: { file },
payload: { file }
});
} else if (node.type === 'mention') {
const text = '@' + node?.attrs?.label + ' ';
if (
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content[content.length - 1].payload.text += text;
} else {
content.push({
type: 'text',
payload: { text: text },
payload: { text: text }
});
}
if (content[content.length - 1]?.payload?.atUserList) {
@ -551,10 +494,10 @@ function parseH5EditorContent() {
try {
for (const child of root.childNodes) {
if (
child.nodeName === '#text'
|| child.nodeName === 'SPAN'
|| (child as HTMLElement).classList?.contains('custom-image-emoji')
|| (child as HTMLElement).classList?.contains('mention')
child.nodeName === '#text' ||
child.nodeName === 'SPAN' ||
(child as HTMLElement).classList?.contains('custom-image-emoji') ||
(child as HTMLElement).classList?.contains('mention')
) {
text += child.nodeValue || (child as any).alt || child.innerHTML || '';
if (child.classList?.contains('mention') && child.id && !atUserList?.includes(child.id)) {
@ -572,9 +515,9 @@ function parseH5EditorContent() {
type: 'text',
payload: {
text,
atUserList,
},
},
atUserList
}
}
];
}
@ -586,8 +529,8 @@ function addEmoji(emojiData: any) {
src: emojiData?.url,
alt: emojiData?.emoji.key,
title: emojiData?.emoji.key,
class: 'emoji',
},
class: 'emoji'
}
});
} else {
const emojiImgNode = document.createElement('img');
@ -714,8 +657,8 @@ watch(
},
{
immediate: true,
deep: true,
},
deep: true
}
);
defineExpose({
@ -726,12 +669,12 @@ defineExpose({
setEditorContent,
getEditorHTML,
insertEditorContent,
blur,
blur
});
</script>
<style scoped lang="scss">
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.message-input-editor {
&-container {
@ -833,7 +776,6 @@ defineExpose({
}
img {
/* stylelint-disable-next-line selector-class-pattern */
&.ProseMirror-selectednode {
outline: 2px solid #68cef8;

View File

@ -8,31 +8,30 @@ export default Image.extend({
...(Image.config as any).addAttributes(),
class: {
default: 'image',
rendered: false,
},
rendered: false
}
};
},
addCommands() {
return {
setImage: options => ({ tr, commands }) => {
setImage:
(options) =>
({ tr, commands }) => {
if ((tr.selection as any)?.node?.type?.name == 'custom-image') {
return commands.updateAttributes('custom-image', options);
} else {
return commands.insertContent({
type: this.name,
attrs: options,
attrs: options
});
}
},
}
};
},
renderHTML({ node, HTMLAttributes }) {
HTMLAttributes.class = (node.attrs.class?.includes('custom-image-') ? '' : 'custom-image-') + node.attrs.class;
return [
'img',
HTMLAttributes,
];
},
return ['img', HTMLAttributes];
}
});

View File

@ -4,32 +4,19 @@
:class="{
'input-quote-container': true,
'input-quote-container-uni': isUniFrameWork,
'input-quote-container-h5': isH5,
'input-quote-container-h5': isH5
}"
>
<div class="input-quote-content">
<div class="max-one-line">
{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }}
</div>
<Icon
class="input-quote-close-icon"
:file="closeIcon"
width="11px"
height="11px"
@onClick="cancelQuote"
/>
<div class="max-one-line">{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }}</div>
<Icon class="input-quote-close-icon" :file="closeIcon" width="11px" height="11px" @onClick="cancelQuote" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue';
import closeIcon from '../../../../assets/icon/icon-close.svg';
import { isH5, isUniFrameWork } from '../../../../utils/env';
@ -41,7 +28,7 @@ interface IProps {
}
const props = withDefaults(defineProps<IProps>(), {
displayType: 'editor',
displayType: 'editor'
});
const TYPES = TUIChatEngine.TYPES;
@ -49,13 +36,13 @@ const quoteMessage = ref<IMessageModel>();
onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
quoteMessage: onQuoteMessageUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
quoteMessage: onQuoteMessageUpdated
});
});

View File

@ -1,60 +1,28 @@
<template>
<div
class="tui-chat"
:class="[isH5 ? 'tui-chat-h5' : '']"
>
<div class="tui-chat" :class="[isH5 ? 'tui-chat-h5' : '']">
<!-- <JoinGroupCard v-if="isH5" /> -->
<div
id="tui-chat-main"
class="tui-chat-main"
@click="closeChatPop"
>
<div id="tui-chat-main" class="tui-chat-main" @click="closeChatPop">
<!-- Safe Tips -->
<div
v-if="isOfficial"
class="tui-chat-safe-tips"
>
<div v-if="isOfficial" class="tui-chat-safe-tips">
<span>
{{
TUITranslateService.t(
"TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。"
'TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。'
)
}}
</span>
<a @click="openComplaintLink(Link.complaint)">
{{ TUITranslateService.t("TUIChat.点此投诉") }}
{{ TUITranslateService.t('TUIChat.点此投诉') }}
</a>
</div>
<MessageGroupApplication
v-if="isGroup"
:key="props.groupID"
:groupID="props.groupID"
/>
<MessageGroupApplication v-if="isGroup" :key="props.groupID" :groupID="props.groupID" />
<!-- Message List -->
<ul
id="messageScrollList"
ref="messageListRef"
class="tui-message-list"
@click="onMessageListBackgroundClick"
>
<p
v-if="!isCompleted"
class="message-more"
@click="getHistoryMessageList"
>
{{ TUITranslateService.t("TUIChat.查看更多") }}
<ul id="messageScrollList" ref="messageListRef" class="tui-message-list" @click="onMessageListBackgroundClick">
<p v-if="!isCompleted" class="message-more" @click="getHistoryMessageList">
{{ TUITranslateService.t('TUIChat.查看更多') }}
</p>
<li
v-for="(item, index) in messageList"
:id="'tui-' + item.ID"
:key="item.ID"
ref="messageElementListRef"
class="message-li"
>
<MessageTimestamp
:currTime="item.time"
:prevTime="index > 0 ? messageList[index - 1].time : 0"
/>
<li v-for="(item, index) in messageList" :id="'tui-' + item.ID" :key="item.ID" ref="messageElementListRef" class="message-li">
<MessageTimestamp :currTime="item.time" :prevTime="index > 0 ? messageList[index - 1].time : 0" />
<div class="message-item">
<MessageTip
v-if="item.type === TYPES.MSG_GRP_TIP || isCreateGroupCustomMessage(item)"
@ -79,7 +47,7 @@
<div
v-else
:class="{
'message-event-bind-div': true,
'message-event-bind-div': true
}"
@longpress="handleToggleMessageItem($event, item, true)"
@click.prevent.right="handleToggleMessageItemForPC($event, item)"
@ -100,31 +68,12 @@
@setReadReceiptPanelVisible="setReadReceiptPanelVisible"
>
<template #messageElement>
<MessageText
v-if="item.type === TYPES.MSG_TEXT"
:content="item.getMessageContent()"
:messageItem="item"
/>
<ProgressMessage
v-else-if="item.type === TYPES.MSG_IMAGE"
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageImage
:content="item.getMessageContent()"
:messageItem="item"
@previewImage="handleImagePreview"
/>
<MessageText v-if="item.type === TYPES.MSG_TEXT" :content="item.getMessageContent()" :messageItem="item" />
<ProgressMessage v-else-if="item.type === TYPES.MSG_IMAGE" :content="item.getMessageContent()" :messageItem="item">
<MessageImage :content="item.getMessageContent()" :messageItem="item" @previewImage="handleImagePreview" />
</ProgressMessage>
<ProgressMessage
v-else-if="item.type === TYPES.MSG_VIDEO"
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageVideo
:content="item.getMessageContent()"
:messageItem="item"
/>
<ProgressMessage v-else-if="item.type === TYPES.MSG_VIDEO" :content="item.getMessageContent()" :messageItem="item">
<MessageVideo :content="item.getMessageContent()" :messageItem="item" />
</ProgressMessage>
<MessageAudio
v-else-if="item.type === TYPES.MSG_AUDIO"
@ -132,40 +81,19 @@
:messageItem="item"
@setAudioPlayed="setAudioPlayed"
/>
<ProgressMessage
v-else-if="item.type === TYPES.MSG_FILE"
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageFile
:content="item.getMessageContent()"
:messageItem="item"
/>
<ProgressMessage v-else-if="item.type === TYPES.MSG_FILE" :content="item.getMessageContent()" :messageItem="item">
<MessageFile :content="item.getMessageContent()" :messageItem="item" />
</ProgressMessage>
<MessageRecord
v-else-if="item.type === TYPES.MSG_MERGER"
:renderData="item.payload"
:messageItem="item"
/>
<MessageFace
v-else-if="item.type === TYPES.MSG_FACE"
:content="item.getMessageContent()"
/>
<MessageLocation
v-else-if="item.type === TYPES.MSG_LOCATION"
:content="item.getMessageContent()"
/>
<MessageRecord v-else-if="item.type === TYPES.MSG_MERGER" :renderData="item.payload" :messageItem="item" />
<MessageFace v-else-if="item.type === TYPES.MSG_FACE" :content="item.getMessageContent()" />
<MessageLocation v-else-if="item.type === TYPES.MSG_LOCATION" :content="item.getMessageContent()" />
<MessageStreamMarkdown
v-else-if="isBotMessage(item)"
:payloadData="item.payload.data"
:message="item"
@onStreaming="scrollStreamMessageToBottom"
/>
<MessageCustom
v-else-if="item.type === TYPES.MSG_CUSTOM"
:content="item.getMessageContent()"
:messageItem="item"
/>
<MessageCustom v-else-if="item.type === TYPES.MSG_CUSTOM" :content="item.getMessageContent()" :messageItem="item" />
</template>
<template #TUIEmojiPlugin>
<TUIEmojiPlugin
@ -192,20 +120,13 @@
@toggleMultipleSelectMode="() => emits('toggleMultipleSelectMode')"
>
<template #TUIEmojiPlugin>
<TUIEmojiPlugin
v-if="isShowEmojiPlugin"
:message="item"
:emojiConfig="emojiConfig"
/>
<TUIEmojiPlugin v-if="isShowEmojiPlugin" :message="item" :emojiConfig="emojiConfig" />
</template>
</MessageTool>
</div>
</li>
</ul>
<ScrollButton
ref="scrollButtonInstanceRef"
@scrollToLatestMessage="scrollToLatestMessage"
/>
<ScrollButton ref="scrollButtonInstanceRef" @scrollToLatestMessage="scrollToLatestMessage" />
<Dialog
v-if="reSendDialogShow"
class="resend-dialog"
@ -217,15 +138,10 @@
@update:show="(e) => (reSendDialogShow = e)"
>
<p class="delDialog-title">
{{ TUITranslateService.t("TUIChat.确认重发该消息?") }}
{{ TUITranslateService.t('TUIChat.确认重发该消息?') }}
</p>
</Dialog>
<ImagePreviewer
v-if="showImagePreview"
:currentImage="currentImagePreview"
:imageList="imageMessageList"
@close="onImagePreviewerClose"
/>
<ImagePreviewer v-if="showImagePreview" :currentImage="currentImagePreview" :imageList="imageMessageList" @close="onImagePreviewerClose" />
<ReadReceiptPanel
v-if="isShowReadUserStatusPanel"
:message="Object.assign({}, readStatusMessage)"
@ -237,13 +153,7 @@
<script lang="ts" setup>
import { ref, nextTick, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue';
import TUIChatEngine, {
IMessageModel,
TUIStore,
StoreName,
TUITranslateService,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { IMessageModel, TUIStore, StoreName, TUITranslateService, TUIChatService } from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { outsideClick, getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api';
import { TUIEmojiPlugin } from '@tencentcloud/tui-emoji-plugin';
@ -306,7 +216,7 @@ const props = withDefaults(defineProps<IProps>(), {
isGroup: false,
groupID: '',
isNotInGroup: false,
isMultipleSelectMode: false,
isMultipleSelectMode: false
});
let groupType: string | undefined;
@ -346,7 +256,7 @@ const currentImagePreview = ref<IMessageModel>();
const imageMessageList = computed(() =>
messageList?.value?.filter((item: IMessageModel) => {
return !item.isRevoked && !item.hasRiskContent && item.type === TYPES.value.MSG_IMAGE;
}),
})
);
// resend message dialog
@ -355,7 +265,7 @@ const resendMessageData = ref();
const isShowEmojiPlugin = computed(() => {
const msgPopMenuExtensionList = TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.MSG_POP_MENU.EXT_ID, {
enabledEmojiPlugin,
enabledEmojiPlugin
});
return msgPopMenuExtensionList.some((item) => {
return item.text === 'TUIEmojiPlugin';
@ -369,15 +279,15 @@ onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
messageList: onMessageListUpdated,
messageSource: onMessageSourceUpdated,
isCompleted: isCompletedUpdated,
isCompleted: isCompletedUpdated
});
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
currentConversationID: onCurrentConversationIDUpdated
});
TUIStore.watch(StoreName.CUSTOM, {
isShowMessagePopMenu: isShowMessagePopMenuUpdated,
isShowMessagePopMenu: isShowMessagePopMenuUpdated
});
});
@ -389,15 +299,15 @@ onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
messageList: onMessageListUpdated,
messageSource: onMessageSourceUpdated,
isCompleted: isCompletedUpdated,
isCompleted: isCompletedUpdated
});
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
currentConversationID: onCurrentConversationIDUpdated
});
TUIStore.unwatch(StoreName.CUSTOM, {
isShowMessagePopMenu: isShowMessagePopMenuUpdated,
isShowMessagePopMenu: isShowMessagePopMenuUpdated
});
messageListRef.value?.removeEventListener('scroll', handelScrollListScroll);
@ -429,11 +339,7 @@ async function onMessageListUpdated(list: IMessageModel[]) {
}
const newLastMessage = messageList.value?.[messageList.value?.length - 1];
if (messageTarget.value) {
if (
messageList.value?.findIndex(
(message: IMessageModel) => message?.ID === messageTarget.value?.ID,
) >= 0
) {
if (messageList.value?.findIndex((message: IMessageModel) => message?.ID === messageTarget.value?.ID) >= 0) {
const tempMessage = messageTarget.value;
messageTarget.value = undefined;
await scrollToPosition({ scrollToMessage: tempMessage });
@ -441,15 +347,12 @@ async function onMessageListUpdated(list: IMessageModel[]) {
}
} else if (beforeHistoryGetScrollHeight.value) {
await scrollToPosition({
scrollToOffset: { bottom: beforeHistoryGetScrollHeight.value },
scrollToOffset: { bottom: beforeHistoryGetScrollHeight.value }
});
beforeHistoryGetScrollHeight.value = 0;
} else if (scrollButtonInstanceRef.value?.isScrollButtonVisible && newLastMessage?.flow === 'in') {
return;
} else if (
newLastMessage?.ID
&& JSON.stringify(oldLastMessage) !== JSON.stringify(newLastMessage)
) {
} else if (newLastMessage?.ID && JSON.stringify(oldLastMessage) !== JSON.stringify(newLastMessage)) {
await scrollToPosition({ scrollToBottom: true });
} else if (hasEmojiReaction && isCurrentListInBottomPosition()) {
await scrollToPosition({ scrollToBottom: true });
@ -462,13 +365,11 @@ async function onMessageListUpdated(list: IMessageModel[]) {
function isCurrentListInBottomPosition() {
return (
messageListRef.value
&& typeof messageListRef.value.scrollTop === 'number'
&& typeof messageListRef.value.scrollHeight === 'number'
&& typeof messageListRef.value.clientHeight === 'number'
&& Math.ceil(
messageListRef.value.scrollTop + messageListRef.value.clientHeight,
) >= messageListRef.value.scrollHeight
messageListRef.value &&
typeof messageListRef.value.scrollTop === 'number' &&
typeof messageListRef.value.scrollHeight === 'number' &&
typeof messageListRef.value.clientHeight === 'number' &&
Math.ceil(messageListRef.value.scrollTop + messageListRef.value.clientHeight) >= messageListRef.value.scrollHeight
);
}
@ -486,9 +387,7 @@ async function scrollToPosition(config: ScrollConfig = {}): Promise<void> {
if (config.scrollToBottom) {
container!.scrollTop = container!.scrollHeight;
} else if (config.scrollToMessage) {
const targetMessageDom = messageElementListRef.value?.find(
(dom: HTMLElement) => dom?.id === `tui-${config.scrollToMessage?.ID}`,
);
const targetMessageDom = messageElementListRef.value?.find((dom: HTMLElement) => dom?.id === `tui-${config.scrollToMessage?.ID}`);
if (targetMessageDom?.scrollIntoView) {
targetMessageDom.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
@ -511,11 +410,7 @@ async function onMessageSourceUpdated(message: IMessageModel) {
// Only the second case needs to add scrollToTarget when listening here
messageTarget.value = message;
if (messageTarget.value) {
if (
messageList.value?.findIndex(
(message: IMessageModel) => message?.ID === messageTarget.value?.ID,
) >= 0
) {
if (messageList.value?.findIndex((message: IMessageModel) => message?.ID === messageTarget.value?.ID) >= 0) {
const tempMessage = messageTarget.value;
messageTarget.value = undefined;
await scrollToPosition({ scrollToMessage: tempMessage });
@ -600,7 +495,7 @@ const handleToggleMessageItemForPC = (e: MouseEvent, message: IMessageModel) =>
domRefs: targetMessageDom.value,
ignoreDomRefs: ignoreDomRefs,
handler: closeChatPop,
button: e.button,
button: e.button
});
filterTopMessageDom(e.target);
});
@ -683,16 +578,16 @@ async function scrollToLatestMessage() {
}
}
const handelScrollListScroll = throttle(function (e: Event) {
const handelScrollListScroll = throttle(
function (e: Event) {
scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e);
}, 150, { leading: true });
},
150,
{ leading: true }
);
async function bindIntersectionObserver() {
if (
!messageList.value
|| !messageListRef.value
|| messageList.value.length === 0
) {
if (!messageList.value || !messageListRef.value || messageList.value.length === 0) {
return;
}
@ -701,32 +596,34 @@ async function bindIntersectionObserver() {
return;
}
const mappingFromIDToMessage: Record<string, {
const mappingFromIDToMessage: Record<
string,
{
msgDom: HTMLElement;
msgModel: IMessageModel | undefined;
}> = {};
}
> = {};
observer?.disconnect();
observer = new IntersectionObserver((entries) => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const { isIntersecting, target } = entry;
if (isIntersecting) {
const { msgDom, msgModel } = mappingFromIDToMessage[target.id];
if (
msgModel
&& !msgModel.readReceiptInfo?.isPeerRead
&& !sentReceiptMessageIDSet.has(msgModel.ID)
) {
if (msgModel && !msgModel.readReceiptInfo?.isPeerRead && !sentReceiptMessageIDSet.has(msgModel.ID)) {
TUIChatService.sendMessageReadReceipt([msgModel]);
sentReceiptMessageIDSet.add(msgModel.ID);
observer?.unobserve(msgDom);
}
}
});
}, {
},
{
root: messageListRef.value,
threshold: 0.7,
});
threshold: 0.7
}
);
const arrayOfMessageLi = messageListRef.value?.querySelectorAll('.message-li');
if (arrayOfMessageLi) {
@ -735,14 +632,10 @@ async function bindIntersectionObserver() {
const matchingMessage = messageList.value.find((message: IMessageModel) => {
return messageElement.id.slice(4) === message.ID;
});
if (
matchingMessage
&& matchingMessage.needReadReceipt
&& matchingMessage.flow === 'in'
) {
if (matchingMessage && matchingMessage.needReadReceipt && matchingMessage.flow === 'in') {
mappingFromIDToMessage[messageElement.id] = {
msgDom: messageElement,
msgModel: matchingMessage,
msgModel: matchingMessage
};
observer?.observe(messageElement);
}
@ -750,7 +643,6 @@ async function bindIntersectionObserver() {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isSignalingMessage = (message: IMessageModel) => {
return message?.type === TYPES.value.MSG_CUSTOM && message?.getSignalingInfo();
};
@ -775,14 +667,17 @@ function onMessageListBackgroundClick() {
emits('closeInputToolBar');
}
watch(() => props.isMultipleSelectMode, (newValue) => {
watch(
() => props.isMultipleSelectMode,
(newValue) => {
if (!newValue) {
changeSelectMessageIDList({
type: 'clearAll',
messageID: '',
messageID: ''
});
}
});
}
);
function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove' | 'clearAll'; messageID: string }) {
// TODO need to delete this
@ -791,35 +686,35 @@ function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove'
} else if (type === 'add' && !multipleSelectedMessageIDList.value.includes(messageID)) {
multipleSelectedMessageIDList.value.push(messageID);
} else if (type === 'remove') {
multipleSelectedMessageIDList.value = multipleSelectedMessageIDList.value.filter(id => id !== messageID);
multipleSelectedMessageIDList.value = multipleSelectedMessageIDList.value.filter((id) => id !== messageID);
}
}
function mergeForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: true,
messageIDList: multipleSelectedMessageIDList.value,
messageIDList: multipleSelectedMessageIDList.value
});
}
function oneByOneForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: false,
messageIDList: multipleSelectedMessageIDList.value,
messageIDList: multipleSelectedMessageIDList.value
});
}
function setAudioPlayed(messageID: string) {
audioPlayedMapping.value = {
...audioPlayedMapping.value,
[messageID]: true,
[messageID]: true
};
}
defineExpose({
oneByOneForwardMessage,
mergeForwardMessage,
scrollToLatestMessage,
scrollToLatestMessage
});
</script>

View File

@ -1,23 +1,23 @@
const Link = {
product: {
label: '产品文档',
url: 'https://cloud.tencent.com/document/product/269/1499#.E7.BE.A4.E7.BB.84.E5.8A.9F.E8.83.BD',
url: 'https://cloud.tencent.com/document/product/269/1499#.E7.BE.A4.E7.BB.84.E5.8A.9F.E8.83.BD'
},
customMessage: {
label: '自定义消息',
url: 'https://web.sdk.qcloud.com/im/doc/zh-cn/SDK.html#createCustomMessage',
url: 'https://web.sdk.qcloud.com/im/doc/zh-cn/SDK.html#createCustomMessage'
},
complaint: {
label: '点此投诉',
url: 'https://cloud.tencent.com/apply/p/xc3oaubi98g',
url: 'https://cloud.tencent.com/apply/p/xc3oaubi98g'
},
implement: {
label: '集成TUICallKit',
url: 'https://cloud.tencent.com/document/product/269/79861',
url: 'https://cloud.tencent.com/document/product/269/79861'
},
purchase: {
label: '开通腾讯实时音视频服务',
url: 'https://cloud.tencent.com/document/product/1640/79968',
},
url: 'https://cloud.tencent.com/document/product/1640/79968'
}
};
export default Link;

View File

@ -1,32 +1,15 @@
<template>
<div
class="message-audio"
:class="[
isMobile && 'message-audio-h5',
message.flow === 'out' && 'reserve',
message.hasRiskContent && 'disable',
]"
:class="[isMobile && 'message-audio-h5', message.flow === 'out' && 'reserve', message.hasRiskContent && 'disable']"
@click.stop="play"
>
<div class="audio-icon-container">
<div :class="{ 'mask': true, 'play': isAudioPlaying }" />
<Icon
class="icon"
width="16px"
height="20px"
:file="audioIcon"
/>
<Icon class="icon" width="16px" height="20px" :file="audioIcon" />
</div>
<span
class="time"
:style="{ width: `${data.second * 10 + 20}px` }"
>
{{ data.second || 1 }} "
</span>
<audio
ref="audioRef"
:src="data.url"
/>
<span class="time" :style="{ width: `${data.second * 10 + 20}px` }"> {{ data.second || 1 }} " </span>
<audio ref="audioRef" :src="data.url" />
</div>
</template>
@ -44,12 +27,12 @@ const emits = defineEmits<IEmits>();
const props = defineProps({
content: {
type: Object,
default: () => ({}),
default: () => ({})
},
messageItem: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const data = ref();
@ -109,7 +92,7 @@ function onAudioPaused() {
}
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
$flow-in-bg-color: #fbfbfb;
$flow-out-bg-color: #dceafd;

View File

@ -1,12 +1,7 @@
<template>
<div :class="containerClassNameList">
<!-- multiple select radio -->
<RadioSelect
v-if="props.isMultipleSelectMode"
class="multiple-select-radio"
:isSelected="isMultipleSelected"
@onChange="toggleMultipleSelect"
/>
<RadioSelect v-if="props.isMultipleSelectMode" class="multiple-select-radio" :isSelected="isMultipleSelected" @onChange="toggleMultipleSelect" />
<div
:class="{
'control-reverse': message.flow === 'out'
@ -14,22 +9,10 @@
>
<!-- message-bubble-container -->
<div class="message-bubble-content">
<div
class="message-bubble-main-content"
:class="[message.flow === 'in' ? '' : 'reverse']"
>
<Avatar
useSkeletonAnimation
:url="message.avatar || ''"
/>
<main
class="message-body"
@click.stop
>
<div
v-if="message.flow === 'in' && message.conversationType === 'GROUP'"
class="message-body-nick-name"
>
<div class="message-bubble-main-content" :class="[message.flow === 'in' ? '' : 'reverse']">
<Avatar useSkeletonAnimation :url="message.avatar || ''" />
<main class="message-body" @click.stop>
<div v-if="message.flow === 'in' && message.conversationType === 'GROUP'" class="message-body-nick-name">
{{ props.content.showName }}
</div>
<div :class="['message-body-main', message.flow === 'out' && 'message-body-main-reverse']">
@ -41,44 +24,29 @@
message.hasRiskContent && 'content-has-risk',
isNoPadding ? 'content-no-padding' : '',
isNoPadding && isBlink ? 'blink-shadow' : '',
!isNoPadding && isBlink ? 'blink-content' : '',
!isNoPadding && isBlink ? 'blink-content' : ''
]"
>
<div class="content-main">
<img
v-if="
(message.type === TYPES.MSG_IMAGE || message.type === TYPES.MSG_VIDEO) &&
message.hasRiskContent
"
v-if="(message.type === TYPES.MSG_IMAGE || message.type === TYPES.MSG_VIDEO) && message.hasRiskContent"
:class="['message-risk-replace', !isPC && 'message-risk-replace-h5']"
:src="riskImageReplaceUrl"
>
/>
<template v-else>
<slot name="messageElement" />
<slot name="TUIEmojiPlugin" />
</template>
</div>
<!-- Risk Content Tips -->
<div
v-if="message.hasRiskContent"
class="content-has-risk-tips"
>
<div v-if="message.hasRiskContent" class="content-has-risk-tips">
{{ riskContentText }}
</div>
</div>
<!-- audio unplay mark -->
<div
v-if="isDisplayUnplayMark"
class="audio-unplay-mark"
/>
<div v-if="isDisplayUnplayMark" class="audio-unplay-mark" />
<!-- Send Fail Icon -->
<div
v-if="message.status === 'fail' || message.hasRiskContent"
class="message-label fail"
@click="resendMessage()"
>
!
</div>
<div v-if="message.status === 'fail' || message.hasRiskContent" class="message-label fail" @click="resendMessage()">!</div>
<!-- Loading Icon -->
<Icon
v-if="message.status === 'unSend' && needLoadingIconMessageType.includes(message.type)"
@ -88,28 +56,16 @@
:height="'15px'"
/>
<!-- Read & Unread -->
<ReadStatus
class="message-label align-self-bottom"
:message="shallowCopyMessage(message)"
@openReadUserPanel="openReadUserPanel"
/>
<ReadStatus class="message-label align-self-bottom" :message="shallowCopyMessage(message)" @openReadUserPanel="openReadUserPanel" />
</div>
</main>
</div>
<!-- message extra area -->
<div
class="message-bubble-extra-content"
>
<div class="message-bubble-extra-content">
<!-- extra: message translation -->
<MessageTranslate
:class="message.flow === 'out' ? 'reverse' : 'flex-row'"
:message="message"
/>
<MessageTranslate :class="message.flow === 'out' ? 'reverse' : 'flex-row'" :message="message" />
<!-- extra: message convert voice to text -->
<MessageConvert
:class="message.flow === 'out' ? 'reverse' : 'flex-row'"
:message="message"
/>
<MessageConvert :class="message.flow === 'out' ? 'reverse' : 'flex-row'" :message="message" />
<!-- extra: message quote -->
<MessageQuote
:class="message.flow === 'out' ? 'reverse' : 'flex-row'"
@ -159,24 +115,18 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
messageItem: () => ({} as IMessageModel),
messageItem: () => ({}) as IMessageModel,
content: () => ({}),
isAudioPlayed: false,
blinkMessageIDList: () => [],
classNameList: () => [],
isMultipleSelectMode: false,
multipleSelectedMessageIDList: () => [],
multipleSelectedMessageIDList: () => []
});
const TYPES = TUIChatEngine.TYPES;
const riskImageReplaceUrl = 'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png';
const needLoadingIconMessageType = [
TYPES.MSG_LOCATION,
TYPES.MSG_TEXT,
TYPES.MSG_CUSTOM,
TYPES.MSG_MERGER,
TYPES.MSG_FACE,
];
const needLoadingIconMessageType = [TYPES.MSG_LOCATION, TYPES.MSG_TEXT, TYPES.MSG_CUSTOM, TYPES.MSG_MERGER, TYPES.MSG_FACE];
const { blinkMessageIDList, messageItem: message } = toRefs(props);
@ -185,18 +135,11 @@ const isMultipleSelected = computed<boolean>(() => {
});
const isDisplayUnplayMark = computed<boolean>(() => {
return message.value.flow === 'in'
&& message.value.status === 'success'
&& message.value.type === TYPES.MSG_AUDIO
&& !props.isAudioPlayed;
return message.value.flow === 'in' && message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO && !props.isAudioPlayed;
});
const containerClassNameList = computed(() => {
return [
'message-bubble',
isMultipleSelected.value ? 'multiple-selected' : '',
...props.classNameList,
];
return ['message-bubble', isMultipleSelected.value ? 'multiple-selected' : '', ...props.classNameList];
});
// When an emoji is deleted, the `reactionList` will update the corresponding emoji's `totalUserCount`.
@ -213,9 +156,7 @@ const riskContentText = computed<string>(() => {
if (message.value.flow === 'out') {
content += TUITranslateService.t('TUIChat.发送失败');
} else {
content += TUITranslateService.t(
message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看',
);
content += TUITranslateService.t(message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看');
}
return content;
});
@ -230,7 +171,7 @@ const isBlink = computed(() => {
function toggleMultipleSelect(isSelected: boolean) {
emits('changeSelectMessageIDList', {
type: isSelected ? 'add' : 'remove',
messageID: message.value.ID,
messageID: message.value.ID
});
}

View File

@ -3,7 +3,7 @@
class="message-convert-container"
:style="{
height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto'
}"
>
<div
@ -11,7 +11,7 @@
ref="convertContentRef"
:class="{
'convert-content': true,
'occur': calculateHeight > 0,
'occur': calculateHeight > 0
}"
>
{{ convertText }}
@ -30,10 +30,7 @@
<script lang="ts" setup>
import { ref, watch, nextTick } from '../../../../../adapter-vue';
import {
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRectSync } from '@tencentcloud/universal-api';
import { convertor } from '../../../utils/convertVoiceToText';
@ -50,8 +47,8 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel),
isSingleConvert: false,
message: () => ({}) as IMessageModel,
isSingleConvert: false
});
const convertFinished = ref<boolean>(false);
@ -62,9 +59,12 @@ const calculateWidth = ref<number>(0);
const convertLoadingRef = ref<HTMLDivElement>();
const convertContentRef = ref<HTMLDivElement>();
watch(() => props.contentVisible, (newVal: boolean) => {
watch(
() => props.contentVisible,
(newVal: boolean) => {
if (newVal) {
convertor.get(props.message)
convertor
.get(props.message)
.then((text) => {
convertFinished.value = true;
convertText.value = text;
@ -99,9 +99,11 @@ watch(() => props.contentVisible, (newVal: boolean) => {
convertText.value = err.message;
});
}
}, {
immediate: true,
});
},
{
immediate: true
}
);
</script>
<style lang="scss" scoped>
@ -109,7 +111,9 @@ watch(() => props.contentVisible, (newVal: boolean) => {
min-height: 20px;
min-width: 80px;
position: relative;
transition: width 0.15s ease-out, height 0.15s ease-out, ;
transition:
width 0.15s ease-out,
height 0.15s ease-out;
font-size: 14px;
.loading {

View File

@ -5,7 +5,7 @@
:class="{
'message-convert': true,
'reverse': props.message.flow === 'out',
'error': hasConvertError,
'error': hasConvertError
}"
>
<ConvertContent
@ -20,11 +20,7 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import {
TUIStore,
StoreName,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IMessageModel } from '@tencentcloud/chat-uikit-engine';
import ConvertContent from './convert-content.vue';
import { IConvertInfo } from '../../../../../interface';
@ -33,7 +29,7 @@ interface IProps {
}
const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel),
message: () => ({}) as IMessageModel
});
const convertVisible = ref<boolean>(false);
@ -44,13 +40,13 @@ let isSingleConvert = true;
onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
voiceToTextInfo: onMessageConvertUpdated,
voiceToTextInfo: onMessageConvertUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
voiceToTextInfo: onMessageConvertUpdated,
voiceToTextInfo: onMessageConvertUpdated
});
});

View File

@ -4,22 +4,11 @@
<div>
<h1>
<label>{{ extension.title }}</label>
<a
v-if="extension.hyperlinks_text"
:href="extension.hyperlinks_text.value"
target="view_window"
>{{ extension.hyperlinks_text.key }}</a>
<a v-if="extension.hyperlinks_text" :href="extension.hyperlinks_text.value" target="view_window">{{ extension.hyperlinks_text.key }}</a>
</h1>
<ul v-if="extension.item && extension.item.length > 0">
<li
v-for="(item, index) in extension.item"
:key="index"
>
<a
v-if="isUrl(item.value)"
:href="item.value"
target="view_window"
>{{ item.key }}</a>
<li v-for="(item, index) in extension.item" :key="index">
<a v-if="isUrl(item.value)" :href="item.value" target="view_window">{{ item.key }}</a>
<p v-else>
{{ item.key }}
</p>
@ -30,30 +19,18 @@
</template>
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.EVALUATE">
<div class="evaluate">
<h1>{{ TUITranslateService.t("message.custom.对本次服务评价") }}</h1>
<h1>{{ TUITranslateService.t('message.custom.对本次服务评价') }}</h1>
<ul class="evaluate-list">
<li
v-for="(item, index) in Math.max(customData.score, 0)"
:key="index"
class="evaluate-list-item"
>
<Icon
:file="star"
class="file-icon"
/>
<li v-for="(item, index) in Math.max(customData.score, 0)" :key="index" class="evaluate-list-item">
<Icon :file="star" class="file-icon" />
</li>
</ul>
<article>{{ customData.comment }}</article>
</div>
</template>
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.ORDER">
<div
class="order"
@click="openLink(customData.link)"
>
<img
:src="customData.imageUrl"
>
<div class="order" @click="openLink(customData.link)">
<img :src="customData.imageUrl" />
<main>
<h1>{{ customData.title }}</h1>
<p>{{ customData.description }}</p>
@ -64,12 +41,7 @@
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.LINK">
<div class="textLink">
<p>{{ customData.text }}</p>
<a
:href="customData.link"
target="view_window"
>{{
TUITranslateService.t("message.custom.查看详情>>")
}}</a>
<a :href="customData.link" target="view_window">{{ TUITranslateService.t('message.custom.查看详情>>') }}</a>
</div>
</template>
<template v-else>
@ -93,14 +65,14 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
messageItem: undefined,
content: undefined,
content: undefined
});
const custom = ref();
const message = ref<IMessageModel>();
const extension = ref();
const customData = ref<ICustomMessagePayload>({
businessID: '',
businessID: ''
});
watchEffect(() => {
@ -118,7 +90,7 @@ const openLink = (url: any) => {
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
a {
color: #679ce1;

View File

@ -1,12 +1,6 @@
<template>
<div
class="message-image"
>
<img
mode="aspectFit"
class="message-image"
:src="url"
>
<div class="message-image">
<img mode="aspectFit" class="message-image" :src="url" />
</div>
</template>
@ -17,8 +11,8 @@ import { CUSTOM_BIG_EMOJI_URL } from '../../emoji-config';
const props = defineProps({
content: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const url = ref(props.content.url);
@ -35,7 +29,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-image {
width: 80px;

View File

@ -1,13 +1,6 @@
<template>
<div
class="file-message-montainer"
:title="TUITranslateService.t('TUIChat.单击下载')"
@click="download"
>
<Icon
:file="files"
class="file-icon"
/>
<div class="file-message-montainer" :title="TUITranslateService.t('TUIChat.单击下载')" @click="download">
<Icon :file="files" class="file-icon" />
<div>
<div>{{ props.content.name }}</div>
<div>{{ props.content.size }}</div>
@ -28,9 +21,9 @@ const props = withDefaults(
messageItem: IMessageModel;
}>(),
{
content: () => ({} as IFileMessageContent),
messageItem: () => ({} as IMessageModel),
},
content: () => ({}) as IFileMessageContent,
messageItem: () => ({}) as IMessageModel
}
);
const download = () => {
@ -40,13 +33,13 @@ const download = () => {
const option = {
mode: 'cors',
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded',
}),
'Content-Type': 'application/x-www-form-urlencoded'
})
} as RequestInit;
// If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page
if ((window as any)?.fetch) {
fetch(props.content.url, option)
.then(res => res.blob())
.then((res) => res.blob())
.then((blob) => {
const a = document.createElement('a');
const url = window.URL.createObjectURL(blob);
@ -64,7 +57,7 @@ const download = () => {
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.file-message-montainer {
display: flex;

View File

@ -1,15 +1,11 @@
<template>
<div
ref="skeletonDomRef"
class="image-container"
@click.self="toggleShow"
>
<div ref="skeletonDomRef" class="image-container" @click.self="toggleShow">
<img
:class="['message-image', !isPC && 'message-image-h5']"
:src="props.content.url"
:width="props.content.width"
:height="props.content.height"
>
/>
</div>
</template>
@ -28,17 +24,13 @@ const props = withDefaults(
}>(),
{
content: () => ({}),
messageItem: () => ({} as IMessageModel),
},
messageItem: () => ({}) as IMessageModel
}
);
const skeletonDomRef = ref();
onMounted(() => {
if (
props.messageItem?.status === 'success'
|| props.messageItem?.status === 'fail'
|| props.messageItem?.progress === 1
) {
if (props.messageItem?.status === 'success' || props.messageItem?.status === 'fail' || props.messageItem?.progress === 1) {
autoFixSkeletonSize();
}
});
@ -49,7 +41,7 @@ watch(
if (newVal > oldVal) {
autoFixSkeletonSize();
}
},
}
);
function autoFixSkeletonSize() {
@ -70,7 +62,7 @@ function toggleShow() {
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.image-container {
overflow: hidden;

View File

@ -1,12 +1,7 @@
<template>
<a
class="message-location"
:href="data.href"
target="_blank"
title="点击查看详情"
>
<a class="message-location" :href="data.href" target="_blank" title="点击查看详情">
<span class="el-icon-location-outline">{{ data.description }}</span>
<img :src="data.url">
<img :src="data.url" />
</a>
</template>
@ -15,8 +10,8 @@ import { watchEffect, ref } from '../../../../adapter-vue';
const props = defineProps({
content: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const data = ref();
watchEffect(() => {
@ -24,7 +19,7 @@ watchEffect(() => {
});
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-location {
display: flex;

View File

@ -3,33 +3,20 @@
v-if="hasQuoteContent"
:class="{
'reference-content': true,
'reverse': message.flow === 'out',
'reverse': message.flow === 'out'
}"
@click="scrollToOriginalMessage"
>
<div
v-if="isMessageRevoked"
class="revoked-text"
>
<div v-if="isMessageRevoked" class="revoked-text">
{{ TUITranslateService.t('TUIChat.引用内容已撤回') }}
</div>
<div
v-else
class="max-double-line"
>
{{ messageQuoteContent.messageSender }}: {{ transformTextWithKeysToEmojiNames(messageQuoteText) }}
</div>
<div v-else class="max-double-line">{{ messageQuoteContent.messageSender }}: {{ transformTextWithKeysToEmojiNames(messageQuoteText) }}</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted } from '../../../../../adapter-vue';
import {
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api';
import { isUniFrameWork } from '../../../../../utils/env';
import { Toast, TOAST_TYPE } from '../../../../../components/common/Toast/index';
@ -47,7 +34,7 @@ export interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel),
message: () => ({}) as IMessageModel
});
let selfAddValue = 0;
@ -116,12 +103,7 @@ function performQuoteContent(params: IQuoteContent) {
messageKey = '[消息]';
break;
}
if (
[
MessageQuoteTypeEnum.TYPE_TEXT,
MessageQuoteTypeEnum.TYPE_MERGER,
].includes(params.messageType)
) {
if ([MessageQuoteTypeEnum.TYPE_TEXT, MessageQuoteTypeEnum.TYPE_MERGER].includes(params.messageType)) {
quoteContent = params.messageAbstract;
}
return quoteContent ? quoteContent : TUITranslateService.t(`TUIChat.${messageKey}`);
@ -133,7 +115,7 @@ async function scrollToOriginalMessage() {
}
const originMessageID = messageQuoteContent.value?.messageID;
const currentMessageList = TUIStore.getData(StoreName.CHAT, 'messageList');
const isOriginalMessageInScreen = currentMessageList.some(msg => msg.ID === originMessageID);
const isOriginalMessageInScreen = currentMessageList.some((msg) => msg.ID === originMessageID);
if (originMessageID && isOriginalMessageInScreen) {
try {
const scrollViewRect = await getBoundingClientRect('#messageScrollList', 'messageList');
@ -156,7 +138,7 @@ async function scrollToOriginalMessage() {
} else {
Toast({
message: TUITranslateService.t('TUIChat.无法定位到原消息'),
type: TOAST_TYPE.WARNING,
type: TOAST_TYPE.WARNING
});
}
}

View File

@ -56,5 +56,5 @@ export enum MessageQuoteTypeEnum {
/**
* merge forward message
*/
TYPE_MERGER = 10,
TYPE_MERGER = 10
}

View File

@ -1,20 +1,11 @@
<template>
<div>
<div
class="message-record-container"
@click="openMergeDetail"
>
<div
class="record-title"
>
<div class="message-record-container" @click="openMergeDetail">
<div class="record-title">
{{ props.renderData.title }}
</div>
<div class="record-abstract-container">
<div
v-for="(item, index) in props.renderData.abstractList.slice(0, 7)"
:key="index"
class="record-abstract-item"
>
<div v-for="(item, index) in props.renderData.abstractList.slice(0, 7)" :key="index" class="record-abstract-item">
{{ transformTextWithKeysToEmojiNames(item) }}
</div>
</div>
@ -22,11 +13,7 @@
{{ TUITranslateService.t('TUIChat.聊天记录') }}
</div>
</div>
<Overlay
v-if="!props.disabled && isPC"
:visible="isMessageListVisible"
@onOverlayClick="isMessageListVisible = false"
>
<Overlay v-if="!props.disabled && isPC" :visible="isMessageListVisible" @onOverlayClick="isMessageListVisible = false">
<SimpleMessageList
:isMounted="isMessageListVisible"
:renderData="props.renderData"
@ -83,7 +70,7 @@ interface IProps {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
messageItem: () => ({}) as IMessageModel,
disabled: false,
disabled: false
});
const isMessageListVisible = ref(false);

View File

@ -1,14 +1,7 @@
<template>
<div class="message-stream">
<pre
ref="messageContentRef"
:class="['message-marked', 'message-typewriter']"
v-html="markedContent"
/>
<StreamOperation
v-show="isOperationShow"
:content="streamContent"
/>
<pre ref="messageContentRef" :class="['message-marked', 'message-typewriter']" v-html="markedContent" />
<StreamOperation v-show="isOperationShow" :content="streamContent" />
</div>
</template>
<script lang="ts" setup>
@ -30,7 +23,7 @@ const props = withDefaults(defineProps<IProps>(), {
payloadData: () => '',
enableMarkdown: true,
enableStreaming: true,
enableOperation: true,
enableOperation: true
});
const emits = defineEmits(['onStreaming']);
@ -54,7 +47,7 @@ const typeWriter = new TypeWriter({
},
onComplete() {
isStreaming.value = false;
},
}
});
const generateMarkedContent = (content: string) => {
@ -77,16 +70,17 @@ function startStreaming(content: string[]) {
}
}
watch(() => props.payloadData,
watch(
() => props.payloadData,
(newValue: string, oldValue: string) => {
if (newValue === oldValue) {
return;
}
if(props.enableMarkdown){
if (props.enableMarkdown) {
TUIReportService.reportFeature(206);
}
if(props.enableStreaming){
if (props.enableStreaming) {
TUIReportService.reportFeature(207);
}
@ -125,14 +119,17 @@ watch(() => props.payloadData,
}
prevChunksLength.value = chunks.value?.length;
}, {
deep: true,
immediate: true,
},
{
deep: true,
immediate: true
}
);
onMounted(() => {
watch(() => isStreaming.value, (newValue: boolean, oldValue: boolean) => {
watch(
() => isStreaming.value,
(newValue: boolean, oldValue: boolean) => {
if (newValue === oldValue) {
return;
}
@ -147,9 +144,11 @@ onMounted(() => {
}
});
}
}, {
immediate: true,
});
},
{
immediate: true
}
);
});
onUnmounted(() => {
@ -168,6 +167,5 @@ function copyCode(event: Event) {
CopyManager.copyTextOrHtml(codeElement.textContent, 'text');
}
}
</script>
<style lang="scss" src="./index.scss"></style>

View File

@ -15,19 +15,19 @@ const escapeReplacements: Iescape = {
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
"'": '&#39;'
};
const getEscapeReplacement = (ch: string): string => escapeReplacements[ch as keyof Iescape];
export const marked = new Marked(
{
mangle: false,
headerIds: false,
headerIds: false
},
markedHighlight({
highlight(code: string) {
return hljs.highlightAuto(code).value;
},
}
}),
{
renderer: {
@ -48,9 +48,9 @@ export const marked = new Marked(
},
link(this: any, href: string | null, title: string | null, text: string) {
return `<a target="_blank" rel="noreferrer noopenner" class="message-marked_link" href="${href}" title="${title}">${text}</a>`;
},
},
},
}
}
}
);
export const markedWithPurify = (text: string) => {

View File

@ -1,25 +1,17 @@
<template>
<div
ref="operationContainerRef"
class="message-stream_operation_container"
>
<div ref="operationContainerRef" class="message-stream_operation_container">
<div class="message-stream_operation_list">
<div
v-for="(operation, key) in operationConfig"
:key="key"
:class="{
'message-stream_operation_item': true,
'message-stream_operation_item_disabled': operation.isDisabled,
'message-stream_operation_item_disabled': operation.isDisabled
}"
:title="operation.name"
@click="(e) => operation.onClick(e, operation.key)"
>
<Icon
class="message-stream_operation_icon"
:file="operation.icon"
:name="operation.name"
size="14px"
/>
<Icon class="message-stream_operation_icon" :file="operation.icon" :name="operation.name" size="14px" />
</div>
</div>
</div>
@ -45,7 +37,7 @@ interface IEmits {
}
const props = withDefaults(defineProps<IProps>(), {
operations: () => [IOperationType.Copy],
operations: () => [IOperationType.Copy]
});
const emits = defineEmits<IEmits>();
@ -57,8 +49,8 @@ const defaultOperationConfig: Record<string, IOperation> = {
isDisabled: false,
onClick: () => {
CopyManager.copyTextOrHtml(props.content, 'text');
},
},
}
}
};
const operationConfig = computed(() => {
@ -76,14 +68,14 @@ const operationConfig = computed(() => {
}
config.onClick(e, key);
emits('onOperationClick', e, key);
},
}
};
})
.filter(Boolean) as IOperation[];
});
</script>
<style lang="scss" scoped>
.message-stream_operation_container {
.message-stream_operation_container {
display: flex;
flex-direction: row;
align-self: flex-end;
@ -105,5 +97,5 @@ const operationConfig = computed(() => {
}
}
}
}
}
</style>

View File

@ -1,6 +1,6 @@
/** type & interface */
export enum IOperationType {
Copy = 'copy',
Copy = 'copy'
// Retry is not supported now
// Retry = 'retry',
}

View File

@ -1,7 +1,7 @@
const chineseRegex = /[\u4e00-\u9fa5]/;
const wordAndNonWordRegex = /\b\w+\b|[^\w]+/g;
const isStringArray = (test: any): boolean => {
return Array.isArray(test) && !test.some(value => typeof value !== 'string');
return Array.isArray(test) && !test.some((value) => typeof value !== 'string');
};
export class TypeWriter {
@ -141,7 +141,7 @@ export class TypeWriter {
return;
}
if ((this.curArrayPos >= this.strings.length)) {
if (this.curArrayPos >= this.strings.length) {
this.isTyping = false;
this.onComplete?.(this);
return;

View File

@ -1,28 +1,13 @@
<template>
<div :class="['message-text-container', isPC && 'text-select']">
<span
v-for="(item, index) in processedContent"
:key="index"
>
<span
v-if="item.name === 'text'"
class="text"
>
<span v-for="(item, index) in processedContent" :key="index">
<span v-if="item.name === 'text'" class="text">
{{ item.text }}
</span>
<span
v-else-if="item.name === 'url'"
class="url-link"
@click="navigateToUrl(item.url)"
>
<span v-else-if="item.name === 'url'" class="url-link" @click="navigateToUrl(item.url)">
{{ item.text }}
</span>
<img
v-else
class="emoji"
:src="item.src"
:alt="item.emojiKey"
>
<img v-else class="emoji" :src="item.src" :alt="item.emojiKey" />
</span>
</div>
</template>
@ -51,8 +36,8 @@ interface TextItem {
const props = withDefaults(defineProps<IProps>(), {
content: () => ({}),
messageItem: () => ({} as IMessageModel),
enableURLHighlight: false,
messageItem: () => ({}) as IMessageModel,
enableURLHighlight: false
});
const processedContent = ref<TextItem>([]);
@ -64,11 +49,11 @@ watch(
return;
}
if(props.enableURLHighlight){
if (props.enableURLHighlight) {
TUIReportService.reportFeature(208);
}
if(props.messageItem.getMessageContent){
if (props.messageItem.getMessageContent) {
processedContent.value = props.messageItem.getMessageContent()?.text;
} else {
processedContent.value = TUIStore.getMessageModel(props.messageItem.ID)?.getMessageContent()?.text;
@ -80,7 +65,8 @@ watch(
return;
}
processedContent.value = processedContent.value.map((item: TextItem) => {
processedContent.value = processedContent.value
.map((item: TextItem) => {
// handle custom emoji
if (item.name === 'img' && item?.type === 'custom') {
if (!CUSTOM_BASIC_EMOJI_URL) {
@ -99,26 +85,27 @@ watch(
// handle url
if (props.enableURLHighlight && item.name === 'text' && item.text) {
if(!parseTextAndValidateUrls){
if (!parseTextAndValidateUrls) {
console.warn('parseTextAndValidateUrls not found. Please update @tencentcloud/universal-api to 2.3.7 or higher.');
return item;
}
const segments = parseTextAndValidateUrls(item.text);
if (segments.length) {
return segments.map((segment)=>({
return segments.map((segment) => ({
name: segment.type,
text: segment.text,
url: segment.url,
url: segment.url
}));
}
}
return item;
})?.flat();
})
?.flat();
},
{
deep: true,
immediate: true,
immediate: true
}
);
@ -152,7 +139,9 @@ function navigateToUrl(url: string) {
user-select: text;
}
.text,.emoji,.url-link{
.text,
.emoji,
.url-link {
&::selection {
background-color: #b4d5fe;
color: inherit;
@ -167,7 +156,8 @@ function navigateToUrl(url: string) {
height: 20px;
}
.text, .url-link {
.text,
.url-link {
font-size: 14px;
white-space: pre-wrap;
word-break: break-all;

View File

@ -1,8 +1,5 @@
<template>
<div
v-if="timestampShowFlag"
class="message-timestamp"
>
<div v-if="timestampShowFlag" class="message-timestamp">
{{ timestampShowContent }}
</div>
</template>
@ -13,12 +10,12 @@ import { calculateTimestamp } from '../../utils/utils';
const props = defineProps({
currTime: {
type: Number,
default: 0,
default: 0
},
prevTime: {
type: Number,
default: 0,
},
default: 0
}
});
const { currTime, prevTime } = toRefs(props);
const timestampShowFlag = ref(false);
@ -48,19 +45,16 @@ watch(
if (newVal?.toString() === oldVal?.toString()) {
return;
} else {
timestampShowContent.value = handleItemTime(
currTime.value,
prevTime.value,
);
timestampShowContent.value = handleItemTime(currTime.value, prevTime.value);
}
},
{
immediate: true,
},
immediate: true
}
);
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-timestamp {
margin: 10px auto;

View File

@ -9,13 +9,13 @@ import { computed } from '../../../../adapter-vue';
const props = defineProps({
content: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const tipContent = computed(() => props.content?.text || props.content?.custom || '');
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-tip {
margin: 0 auto;

View File

@ -5,7 +5,7 @@
:class="{
'message-translation': true,
'reverse': props.message.flow === 'out',
'error': hasTranslationError,
'error': hasTranslationError
}"
>
<TranslationContent
@ -16,10 +16,7 @@
@toggleErrorStatus="toggleErrorStatus"
/>
<div class="copyright">
<Icon
:file="checkIcon"
size="13px"
/>
<Icon :file="checkIcon" size="13px" />
<div class="copyright-text">
{{ TUITranslateService.t('TUIChat.由IM提供翻译支持') }}
</div>
@ -29,12 +26,7 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import {
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../../common/Icon.vue';
import TranslationContent from './translation-content.vue';
import checkIcon from '../../../../../assets/icon/check-sm.svg';
@ -45,7 +37,7 @@ interface IProps {
}
const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel),
message: () => ({}) as IMessageModel
});
const translationVisible = ref<boolean>(false);
@ -57,13 +49,13 @@ let isSingleTranslation = true;
onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationUpdated,
translateTextInfo: onMessageTranslationUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationUpdated,
translateTextInfo: onMessageTranslationUpdated
});
});

View File

@ -3,7 +3,7 @@
class="message-translation-container"
:style="{
height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto'
}"
>
<div
@ -11,25 +11,13 @@
ref="translationContentRef"
:class="{
'translation-content': true,
'occur': calculateHeight > 0,
'occur': calculateHeight > 0
}"
>
<template
v-if="translationTextList.length > 0"
>
<span
v-for="(text, index) in translationTextList"
:key="index"
>
<img
v-if="text.type === 'face'"
class="text-face"
:src="text.value"
>
<span
v-else
class="text-plain"
>{{ text.value }}</span>
<template v-if="translationTextList.length > 0">
<span v-for="(text, index) in translationTextList" :key="index">
<img v-if="text.type === 'face'" class="text-face" :src="text.value" />
<span v-else class="text-plain">{{ text.value }}</span>
</span>
</template>
<template v-else>
@ -50,10 +38,7 @@
<script lang="ts" setup>
import { ref, watch, nextTick } from '../../../../../adapter-vue';
import {
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRectSync } from '@tencentcloud/universal-api';
import { TranslationTextType, translator } from '../../../utils/translation';
@ -70,7 +55,7 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel),
message: () => ({}) as IMessageModel
});
const translationFinished = ref<boolean>(false);
@ -82,9 +67,12 @@ const calculateWidth = ref<number>(0);
const translationLoadingRef = ref<HTMLDivElement>();
const translationContentRef = ref<HTMLDivElement>();
watch(() => props.translationContentVisible, (newVal: boolean) => {
watch(
() => props.translationContentVisible,
(newVal: boolean) => {
if (newVal) {
translator.get(props.message)
translator
.get(props.message)
.then((result) => {
translationFinished.value = true;
translationTextList.value = result;
@ -121,7 +109,9 @@ watch(() => props.translationContentVisible, (newVal: boolean) => {
translationErrorText.value = err.message;
});
}
}, { immediate: true });
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
@ -129,7 +119,9 @@ watch(() => props.translationContentVisible, (newVal: boolean) => {
min-height: 16px;
min-width: 80px;
position: relative;
transition: width 0.15s ease-out, height 0.15s ease-out, ;
transition:
width 0.15s ease-out,
height 0.15s ease-out;
font-size: 14px;
.loading {

View File

@ -3,21 +3,15 @@
<div
ref="skeleton"
class="message-video-box"
:class="[
(!props.messageItem.progress || props.messageItem.progress === 1)
&& !isPC
&& 'message-video-cover',
]"
:class="[(!props.messageItem.progress || props.messageItem.progress === 1) && !isPC && 'message-video-cover']"
@click="toggleVideoPreviewer"
>
<img
v-if="(props.messageItem.progress > 0 && props.messageItem.progress < 1 && poster) ||
(!isPC && poster)
"
v-if="(props.messageItem.progress > 0 && props.messageItem.progress < 1 && poster) || (!isPC && poster)"
class="message-img"
:class="[isWidth ? 'is-width' : 'is-height']"
:src="poster"
>
/>
<video
v-else-if="!isPC"
ref="videoRef"
@ -27,51 +21,21 @@
preload="auto"
muted
/>
<video
v-else
ref="videoRef"
class="message-img video-web"
:src="props.content.url"
controls
preload="metadata"
:poster="poster"
/>
<video v-else ref="videoRef" class="message-img video-web" :src="props.content.url" controls preload="metadata" :poster="poster" />
</div>
<div
v-if="isShow && !isPC"
class="dialog-video"
>
<div
class="dialog-video-close"
@click.stop="toggleVideoPreviewer"
>
<div v-if="isShow && !isPC" class="dialog-video">
<div class="dialog-video-close" @click.stop="toggleVideoPreviewer">
<Icon :file="closeSVG" />
</div>
<div
class="dialog-video-box"
:class="[!isPC ? 'dialog-video-h5' : '']"
@click.self="toggleVideoPreviewer"
>
<video
:class="[isWidth ? 'is-width' : 'is-height']"
:src="props.content.url"
controls
autoplay
/>
<div class="dialog-video-box" :class="[!isPC ? 'dialog-video-h5' : '']" @click.self="toggleVideoPreviewer">
<video :class="[isWidth ? 'is-width' : 'is-height']" :src="props.content.url" controls autoplay />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
ref,
watch,
computed,
nextTick,
watchEffect,
withDefaults,
} from '../../../../adapter-vue';
import { ref, watch, computed, nextTick, watchEffect, withDefaults } from '../../../../adapter-vue';
import { IMessageModel } from '@tencentcloud/chat-uikit-engine';
import { handleSkeletonSize } from '../../utils/utils';
import Icon from '../../../common/Icon.vue';
@ -85,9 +49,9 @@ const props = withDefaults(
messageItem: IMessageModel;
}>(),
{
content: () => ({} as IVideoMessageContent),
messageItem: () => ({} as IMessageModel),
},
content: () => ({}) as IVideoMessageContent,
messageItem: () => ({}) as IMessageModel
}
);
const emits = defineEmits(['uploading']);
@ -103,8 +67,7 @@ watchEffect(async () => {
if (!props.content) return;
poster.value = await handlePosterUrl(props.content, props.messageItem);
nextTick(async () => {
const containerWidth
= document.getElementById('messageScrollList')?.clientWidth || 0;
const containerWidth = document.getElementById('messageScrollList')?.clientWidth || 0;
const max = !isPC ? Math.min(containerWidth - 172, 300) : 300;
let size;
if (props.messageItem.status === 'success') {
@ -116,10 +79,8 @@ watchEffect(async () => {
snapshotHeight = posterHeight.value;
}
size = handleSkeletonSize(snapshotWidth, snapshotHeight, max, max);
skeleton?.value?.style
&& (skeleton.value.style.width = `${size.width}px`);
skeleton?.value?.style
&& (skeleton.value.style.height = `${size.height}px`);
skeleton?.value?.style && (skeleton.value.style.width = `${size.width}px`);
skeleton?.value?.style && (skeleton.value.style.height = `${size.height}px`);
if (isPC) {
videoRef?.value?.style && (videoRef.value.style.width = `${size.width}px`);
videoRef?.value?.style && (videoRef.value.style.height = `${size.height}px`);
@ -135,11 +96,14 @@ const isWidth = computed(() => {
return snapshotWidth >= snapshotHeight;
});
watch(() => props.messageItem.status, (newVal: string, oldVal: string) => {
watch(
() => props.messageItem.status,
(newVal: string, oldVal: string) => {
if (newVal === 'success' && oldVal !== 'success') {
emits('uploading');
}
});
}
);
function toggleVideoPreviewer() {
// Video upload process does not support full-screen playback.
@ -172,7 +136,7 @@ function getVideoBase64(url: string) {
posterHeight.value = height;
resolve(dataURL);
},
{ once: true },
{ once: true }
);
});
}
@ -183,19 +147,17 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
return await getVideoBase64(messgeContent.url);
} else {
return (
(messgeContent.snapshotUrl !== transparentPosterUrl && messgeContent.snapshotUrl)
|| (messageItem?.payload?.snapshotUrl !== transparentPosterUrl
&& messageItem?.payload?.snapshotUrl)
|| (messageItem.payload?.thumbUrl !== transparentPosterUrl
&& messageItem?.payload?.thumbUrl)
|| (await getVideoBase64(messgeContent.url))
(messgeContent.snapshotUrl !== transparentPosterUrl && messgeContent.snapshotUrl) ||
(messageItem?.payload?.snapshotUrl !== transparentPosterUrl && messageItem?.payload?.snapshotUrl) ||
(messageItem.payload?.thumbUrl !== transparentPosterUrl && messageItem?.payload?.thumbUrl) ||
(await getVideoBase64(messgeContent.url))
);
}
}
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.message-video {
position: relative;
@ -223,7 +185,7 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
border-radius: 10px;
}
img[src=""],
img[src=''],
img:not([src]) {
opacity: 0;
}
@ -236,7 +198,7 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
&::before {
position: absolute;
z-index: 1;
content: "";
content: '';
width: 0;
height: 0;
border: 10px solid transparent;

View File

@ -4,7 +4,7 @@
:class="{
'message-label': true,
'unread': isUseUnreadStyle,
'finger-point': isHoverFingerPointer,
'finger-point': isHoverFingerPointer
}"
@click="openReadUserPanel"
>
@ -14,12 +14,7 @@
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import TUIChatEngine, {
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import TUIChatConfig from '../../../config';
interface IProps {
@ -32,7 +27,7 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
message: () => ({}) as IMessageModel,
message: () => ({}) as IMessageModel
});
const ReadStatus = TUIChatConfig.getFeatureConfig('ReadStatus');
@ -41,7 +36,7 @@ enum ReadState {
Unread,
AllRead,
NotShow,
PartiallyRead,
PartiallyRead
}
const TYPES = TUIChatEngine.TYPES;
@ -50,13 +45,13 @@ const isDisplayMessageReadReceipt = ref<boolean>(TUIStore.getData(StoreName.USER
onMounted(() => {
TUIStore.watch(StoreName.USER, {
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate,
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.USER, {
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate,
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate
});
});
@ -67,16 +62,7 @@ const isShowReadStatus = computed<boolean>(() => {
if (!isDisplayMessageReadReceipt.value) {
return false;
}
const {
ID,
type,
flow,
status,
hasRiskContent,
conversationID,
conversationType,
needReadReceipt = false,
} = props.message;
const { ID, type, flow, status, hasRiskContent, conversationID, conversationType, needReadReceipt = false } = props.message;
// Asynchronous message strike: Determine if there is risky content after the message has been sent
if (hasRiskContent) {
@ -163,9 +149,9 @@ const isUseUnreadStyle = computed(() => {
const isHoverFingerPointer = computed<boolean>(() => {
return (
props.message.needReadReceipt
&& props.message.conversationType === 'GROUP'
&& (readState.value === ReadState.PartiallyRead || readState.value === ReadState.Unread)
props.message.needReadReceipt &&
props.message.conversationType === 'GROUP' &&
(readState.value === ReadState.PartiallyRead || readState.value === ReadState.Unread)
);
});

View File

@ -2,19 +2,12 @@
<div
:class="{
'simple-message-list-container': true,
'simple-message-list-container-mobile': isMobile,
'simple-message-list-container-mobile': isMobile
}"
>
<div class="header-container">
<span
class="back"
@click="backPreviousLevel"
>
<Icon
class="close-icon"
:file="addIcon"
:size="'18px'"
/>
<span class="back" @click="backPreviousLevel">
<Icon class="close-icon" :file="addIcon" :size="'18px'" />
<span v-if="isReturn">{{ TUITranslateService.t('TUIChat.返回') }}</span>
<span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span>
</span>
@ -23,135 +16,61 @@
{{ currentMergeMessageInfo.title }}
</span>
</div>
<div v-if="isDownloadOccurError">
Load Merge Message Error
</div>
<div
v-else-if="isMergeMessageInfoLoaded"
ref="simpleMessageListRef"
class="message-list"
>
<div v-if="isDownloadOccurError">Load Merge Message Error</div>
<div v-else-if="isMergeMessageInfoLoaded" ref="simpleMessageListRef" class="message-list">
<div
v-for="item in currentMergeMessageInfo.messageList"
:key="item.ID"
:class="{
'message-item': true,
'message-item': true
}"
>
<MessageContainer
:sender="item.nick"
:avatar="item.avatar"
:type="item.messageBody[0].type"
:time="item.time"
>
<MessageContainer :sender="item.nick" :avatar="item.avatar" :type="item.messageBody[0].type" :time="item.time">
<!-- text -->
<div
v-if="item.messageBody[0].type === TYPES.MSG_TEXT"
class="message-text"
>
<div v-if="item.messageBody[0].type === TYPES.MSG_TEXT" class="message-text">
<span
v-for="(textInfo, index) in parseTextToRenderArray(item.messageBody[0].payload['text'])"
:key="index"
class="message-text-container"
>
<span
v-if="textInfo.type === 'text'"
class="text"
>{{ textInfo.content }}</span>
<img
v-else
class="simple-emoji"
:src="textInfo.content"
alt="small-face"
>
<span v-if="textInfo.type === 'text'" class="text">{{ textInfo.content }}</span>
<img v-else class="simple-emoji" :src="textInfo.content" alt="small-face" />
</span>
</div>
<!-- image -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_IMAGE"
class="message-image"
>
<img
class="image"
:src="(item.messageBody[0].payload)['imageInfoArray'][2]['url']"
mode="widthFix"
alt="image"
>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_IMAGE" class="message-image">
<img class="image" :src="item.messageBody[0].payload['imageInfoArray'][2]['url']" mode="widthFix" alt="image" />
</div>
<!-- video -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_VIDEO"
class="message-video"
>
<div
v-if="isUniFrameWork"
@click="previewVideoInUniapp((item.messageBody[0].payload)['remoteVideoUrl'])"
>
<image
class="image"
:src="(item.messageBody[0].payload)['thumbUrl']"
mode="widthFix"
alt="image"
/>
<Icon
class="video-play-icon"
:file="playIcon"
/>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_VIDEO" class="message-video">
<div v-if="isUniFrameWork" @click="previewVideoInUniapp(item.messageBody[0].payload['remoteVideoUrl'])">
<image class="image" :src="item.messageBody[0].payload['thumbUrl']" mode="widthFix" alt="image" />
<Icon class="video-play-icon" :file="playIcon" />
</div>
<video
v-else
class="video"
controls
:poster="(item.messageBody[0].payload)['thumbUrl']"
>
<source
:src="(item.messageBody[0].payload)['remoteVideoUrl']"
type="video/mp4"
>
<video v-else class="video" controls :poster="item.messageBody[0].payload['thumbUrl']">
<source :src="item.messageBody[0].payload['remoteVideoUrl']" type="video/mp4" />
</video>
</div>
<!-- audio -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO"
class="message-audio"
>
<span>{{ TUITranslateService.t("TUIChat.语音") }}&nbsp;</span>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO" class="message-audio">
<span>{{ TUITranslateService.t('TUIChat.语音') }}&nbsp;</span>
<span>{{ item.messageBody[0].payload.second }}s</span>
</div>
<!-- big face -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_FACE"
class="message-face"
>
<img
class="image"
:src="resolveBigFaceUrl(item.messageBody[0].payload.data)"
alt="face"
>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_FACE" class="message-face">
<img class="image" :src="resolveBigFaceUrl(item.messageBody[0].payload.data)" alt="face" />
</div>
<!-- file -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_FILE"
class="message-file"
>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_FILE" class="message-file">
{{ TUITranslateService.t('TUIChat.[文件]') }}
</div>
<!-- location -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_LOCATION"
>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_LOCATION">
{{ TUITranslateService.t('TUIChat.[地理位置]') }}
</div>
<!-- merger -->
<div
v-else-if="item.messageBody[0].type === TYPES.MSG_MERGER"
class="message-merger"
@click.capture="entryNextLevel($event, item)"
>
<MessageRecord
disabled
:renderData="item.messageBody[0].payload"
/>
<div v-else-if="item.messageBody[0].type === TYPES.MSG_MERGER" class="message-merger" @click.capture="entryNextLevel($event, item)">
<MessageRecord disabled :renderData="item.messageBody[0].payload" />
</div>
<!-- custom -->
<div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM">
@ -165,11 +84,7 @@
<script setup lang="ts">
import { computed, ref, watch } from '../../../../../adapter-vue';
import TUIChatEngine, {
TUIStore,
TUIChatService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, TUIChatService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import addIcon from '../../../../../assets/icon/back.svg';
import playIcon from '../../../../../assets/icon/video-play.png';
import Icon from '../../../../common/Icon.vue';
@ -197,7 +112,7 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
messageID: '',
isMounted: false,
isMounted: false
});
const TYPES = TUIChatEngine.TYPES;
@ -205,11 +120,13 @@ const isDownloadOccurError = ref(false);
const messageListStack = ref<IMergeMessageContent[]>([]);
const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({
title: '',
messageList: [],
messageList: []
});
const simpleMessageListRef = ref<HTMLElement>();
watch(() => messageListStack.value.length, async (newValue) => {
watch(
() => messageListStack.value.length,
async (newValue) => {
isDownloadOccurError.value = false;
if (newValue < 1) {
return;
@ -219,7 +136,7 @@ watch(() => messageListStack.value.length, async (newValue) => {
try {
const res = await TUIChatService.downloadMergedMessages({
payload: stackTopMessageInfo,
type: TUIChatEngine.TYPES.MSG_MERGER,
type: TUIChatEngine.TYPES.MSG_MERGER
} as any);
// if download complete message, cover the original message in stack top
messageListStack.value[messageListStack.value.length - 1] = res.payload;
@ -228,9 +145,12 @@ watch(() => messageListStack.value.length, async (newValue) => {
}
}
currentMergeMessageInfo.value = messageListStack.value[messageListStack.value.length - 1];
});
}
);
watch(() => props.isMounted, (newValue) => {
watch(
() => props.isMounted,
(newValue) => {
// For compatibility with uniapp, use watch to implement onMounted
if (newValue) {
if (!props.messageID) {
@ -241,9 +161,11 @@ watch(() => props.isMounted, (newValue) => {
} else {
messageListStack.value = [];
}
}, {
immediate: true,
});
},
{
immediate: true
}
);
const isReturn = computed(() => {
return messageListStack.value.length > 1;
@ -269,7 +191,7 @@ function previewVideoInUniapp(url: string) {
if (isUniFrameWork) {
const encodedUrl = encodeURIComponent(url);
uni.navigateTo({
url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`,
url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`
});
}
}
@ -291,7 +213,7 @@ function resolveBigFaceUrl(bigFaceKey: string): string {
</script>
<style scoped lang="scss">
:not(not){
:not(not) {
display: flex;
flex-direction: column;
min-width: 0;
@ -344,7 +266,6 @@ function resolveBigFaceUrl(bigFaceKey: string): string {
text-overflow: ellipsis;
white-space: nowrap;
}
}
.message-list {

View File

@ -17,7 +17,7 @@
<slot />
</div>
<div class="timestamp">
{{ calculateTimestamp(props.time*1000) }}
{{ calculateTimestamp(props.time * 1000) }}
</div>
</div>
</div>
@ -39,7 +39,7 @@ interface IProps {
const props = withDefaults(defineProps<IProps>(), {
sender: '',
avatar: '',
avatar: ''
});
const TYPES = TUIChatEngine.TYPES;
@ -50,7 +50,7 @@ const isNoPadding = computed(() => {
</script>
<style scoped lang="scss">
:not(not){
:not(not) {
display: flex;
flex-direction: column;
min-width: 0;

View File

@ -1,17 +1,9 @@
<template>
<div>
<div
v-if="groupApplicationCount > 0"
class="application-tips"
>
<div>
{{ groupApplicationCount }}{{ TUITranslateService.t("TUIChat.条入群申请") }}
</div>
<div
class="application-tips-btn"
@click="toggleGroupApplicationDrawerShow"
>
{{ TUITranslateService.t("TUIChat.点击处理") }}
<div v-if="groupApplicationCount > 0" class="application-tips">
<div>{{ groupApplicationCount }}{{ TUITranslateService.t('TUIChat.条入群申请') }}</div>
<div class="application-tips-btn" @click="toggleGroupApplicationDrawerShow">
{{ TUITranslateService.t('TUIChat.点击处理') }}
</div>
</div>
<Drawer
@ -25,12 +17,12 @@
bottom: {
minHeight: '60vh',
maxHeight: '80vh',
borderRadius: '12px 12px 0 0',
borderRadius: '12px 12px 0 0'
},
right: {
width: '360px',
borderRadius: '12px 0 0 12px',
boxShadow: '0 0 10px 0 #d0d0d0',
boxShadow: '0 0 10px 0 #d0d0d0'
}
}"
@onOverlayClick="toggleGroupApplicationDrawerShow"
@ -41,18 +33,10 @@
}"
>
<header class="application-header">
<div
@click="toggleGroupApplicationDrawerShow"
>
<Icon
v-if="isPC"
:file="closeIcon"
:size="'16px'"
/>
<div @click="toggleGroupApplicationDrawerShow">
<Icon v-if="isPC" :file="closeIcon" :size="'16px'" />
<div v-else>
{{
TUITranslateService.t('关闭')
}}
{{ TUITranslateService.t('关闭') }}
</div>
</div>
</header>
@ -62,12 +46,12 @@
:key="item.nick"
:class="{
'application-item': true,
'removed': item.isRemoved,
'removed': item.isRemoved
}"
>
<Avatar
:style="{
flex: '0 0 auto',
flex: '0 0 auto'
}"
:url="item.avatar"
:useSkeletonAnimation="true"
@ -77,23 +61,15 @@
{{ item.nick }}
</div>
<div class="application-item-note">
{{ TUITranslateService.t("TUIChat.申请加入") }}
{{ TUITranslateService.t('TUIChat.申请加入') }}
</div>
</div>
<div
class="application-item-operation"
>
<div
class="agree"
@click="handleApplication(item, 'Agree', index)"
>
{{ TUITranslateService.t("TUIChat.同意") }}
<div class="application-item-operation">
<div class="agree" @click="handleApplication(item, 'Agree', index)">
{{ TUITranslateService.t('TUIChat.同意') }}
</div>
<div
class="reject"
@click="handleApplication(item, 'Reject', index)"
>
{{ TUITranslateService.t("TUIChat.拒绝") }}
<div class="reject" @click="handleApplication(item, 'Reject', index)">
{{ TUITranslateService.t('TUIChat.拒绝') }}
</div>
</div>
</div>
@ -105,13 +81,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from '../../../../adapter-vue';
import {
TUIStore,
StoreName,
TUITranslateService,
TUIUserService,
TUIGroupService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, TUITranslateService, TUIUserService, TUIGroupService } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue';
import Avatar from '../../../common/Avatar/index.vue';
import Drawer from '../../../common/Drawer/index.vue';
@ -130,7 +100,7 @@ interface ICustomGroupApplication {
}
const props = withDefaults(defineProps<IProps>(), {
groupID: '',
groupID: ''
});
const drawerDomInstanceRef = ref<InstanceType<typeof Drawer>>();
@ -147,11 +117,14 @@ watch(isGroupApplicationDrawerShow, (newVal) => {
}
});
watch(() => customGroupApplicationList.value.length, (newVal, oldVal) => {
watch(
() => customGroupApplicationList.value.length,
(newVal, oldVal) => {
if (oldVal > 0 && newVal === 0) {
isGroupApplicationDrawerShow.value = false;
}
});
}
);
/**
* Retrieves the current group application list based on the provided groupID.
@ -160,7 +133,7 @@ watch(() => customGroupApplicationList.value.length, (newVal, oldVal) => {
*/
async function getCurrentGroupApplicationList(): Promise<IGroupApplication[]> {
const result: IChatResponese<{ applicationList: IGroupApplication[] }> = await TUIGroupService.getGroupApplicationList();
const currentGroupApplicationList = result.data.applicationList.filter(application => application.groupID === props.groupID);
const currentGroupApplicationList = result.data.applicationList.filter((application) => application.groupID === props.groupID);
return currentGroupApplicationList;
}
@ -173,8 +146,8 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
if (applicationList.length === 0) {
return [];
}
const userIDList = applicationList.map(application => application.applicationType === 0 ? application.applicant : application.userID);
const { data: userProfileList } = await TUIUserService.getUserProfile({ userIDList }) as IChatResponese<IUserProfile[]>;
const userIDList = applicationList.map((application) => (application.applicationType === 0 ? application.applicant : application.userID));
const { data: userProfileList } = (await TUIUserService.getUserProfile({ userIDList })) as IChatResponese<IUserProfile[]>;
const mappingFromUserID2Profile: Record<string, IUserProfile> = {};
userProfileList.forEach((profile: IUserProfile) => {
mappingFromUserID2Profile[profile.userID] = profile;
@ -185,7 +158,7 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
nick: profile.nick || profile.userID || 'anonymous',
avatar: profile.avatar || '',
isRemoved: false,
application: application,
application: application
};
});
@ -195,14 +168,16 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
function handleApplication(customApplication: ICustomGroupApplication, action: 'Agree' | 'Reject', index: number) {
TUIGroupService.handleGroupApplication({
handleAction: action,
application: customApplication.application,
}).then(() => {
application: customApplication.application
})
.then(() => {
customGroupApplicationList.value[index].isRemoved = true;
setTimeout(() => {
customGroupApplicationList.value.splice(index, 1);
groupApplicationCount.value -= 1;
}, 150);
}).catch(() => {
})
.catch(() => {
// TODO: handle error
});
}
@ -215,13 +190,13 @@ onMounted(() => {
});
TUIStore.watch(StoreName.GRP, {
groupSystemNoticeList: onGroupSystemNoticeListUpdated,
groupSystemNoticeList: onGroupSystemNoticeListUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.GRP, {
groupSystemNoticeList: onGroupSystemNoticeListUpdated,
groupSystemNoticeList: onGroupSystemNoticeListUpdated
});
});
@ -316,15 +291,15 @@ function onGroupSystemNoticeListUpdated() {
flex: 0 0 auto;
font-size: 14px;
.agree{
.agree {
color: #679ce1;
cursor: pointer
cursor: pointer;
}
.reject{
.reject {
margin-left: 12px;
color: #fb355d;
cursor: pointer
cursor: pointer;
}
}
}

View File

@ -4,26 +4,11 @@
ref="messageToolDom"
:class="['dialog-item', !isPC ? 'dialog-item-h5' : 'dialog-item-web']"
>
<slot
v-if="featureConfig.EmojiReaction"
name="TUIEmojiPlugin"
/>
<div
class="dialog-item-list"
:class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'"
>
<slot v-if="featureConfig.EmojiReaction" name="TUIEmojiPlugin" />
<div class="dialog-item-list" :class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'">
<template v-for="(item, index) in actionItems">
<div
v-if="item.renderCondition()"
:key="item.key"
class="list-item"
@click="getFunction(index)"
@mousedown="beforeCopy(item.key)"
>
<Icon
:file="item.iconUrl"
:size="'15px'"
/>
<div v-if="item.renderCondition()" :key="item.key" class="list-item" @click="getFunction(index)" @mousedown="beforeCopy(item.key)">
<Icon :file="item.iconUrl" :size="'15px'" />
<span class="list-item-text">{{ item.text }}</span>
</div>
</template>
@ -32,12 +17,7 @@
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, watchEffect, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
import Icon from '../../../common/Icon.vue';
@ -73,7 +53,7 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
isMultipleSelectMode: false,
messageItem: () => ({}) as IMessageModel,
messageItem: () => ({}) as IMessageModel
});
const featureConfig = TUIChatConfig.getFeatureConfig();
@ -86,11 +66,9 @@ const actionItems = ref([
iconUrl: copyIcon,
renderCondition() {
if (!featureConfig.DownloadFile || !message.value) return false;
return isPC && (message.value?.type === TYPES.MSG_FILE
|| message.value.type === TYPES.MSG_VIDEO
|| message.value.type === TYPES.MSG_IMAGE);
return isPC && (message.value?.type === TYPES.MSG_FILE || message.value.type === TYPES.MSG_VIDEO || message.value.type === TYPES.MSG_IMAGE);
},
clickEvent: openMessage,
clickEvent: openMessage
},
{
key: 'copy',
@ -100,7 +78,7 @@ const actionItems = ref([
if (!featureConfig.CopyMessage || !message.value) return false;
return message.value.type === TYPES.MSG_TEXT;
},
clickEvent: copyMessage,
clickEvent: copyMessage
},
{
key: 'revoke',
@ -110,7 +88,7 @@ const actionItems = ref([
if (!featureConfig.RevokeMessage || !message.value) return false;
return message.value.flow === 'out' && message.value.status === 'success';
},
clickEvent: revokeMessage,
clickEvent: revokeMessage
},
{
key: 'delete',
@ -120,7 +98,7 @@ const actionItems = ref([
if (!featureConfig.DeleteMessage || !message.value) return false;
return message.value.status === 'success';
},
clickEvent: deleteMessage,
clickEvent: deleteMessage
},
{
key: 'forward',
@ -130,7 +108,7 @@ const actionItems = ref([
if (!featureConfig.ForwardMessage || !message.value) return false;
return message.value.status === 'success';
},
clickEvent: forwardSingleMessage,
clickEvent: forwardSingleMessage
},
{
key: 'quote',
@ -141,7 +119,7 @@ const actionItems = ref([
const _message = TUIStore.getMessageModel(message.value.ID);
return message.value.status === 'success' && !_message.getSignalingInfo();
},
clickEvent: quoteMessage,
clickEvent: quoteMessage
},
{
key: 'translate',
@ -152,7 +130,7 @@ const actionItems = ref([
if (!featureConfig.TranslateMessage || !message.value) return false;
return message.value.status === 'success' && message.value.type === TYPES.MSG_TEXT;
},
clickEvent: translateMessage,
clickEvent: translateMessage
},
{
key: 'convert',
@ -163,7 +141,7 @@ const actionItems = ref([
if (!featureConfig.VoiceToText || !message.value) return false;
return message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO;
},
clickEvent: convertVoiceToText,
clickEvent: convertVoiceToText
},
{
key: 'multi-select',
@ -173,8 +151,8 @@ const actionItems = ref([
if (!featureConfig.MultiSelection || !message.value) return false;
return message.value.status === 'success';
},
clickEvent: multipleSelectMessage,
},
clickEvent: multipleSelectMessage
}
]);
const message = ref<IMessageModel>();
@ -183,14 +161,14 @@ const messageToolDom = ref<HTMLElement>();
onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated
});
});
@ -242,7 +220,7 @@ function revokeMessage() {
const message = TUITranslateService.t('TUIChat.已过撤回时限');
Toast({
message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
});
@ -257,7 +235,7 @@ function deleteMessage() {
async function copyMessage() {
if (isUniFrameWork) {
TUIGlobal?.setClipboardData({
data: transformTextWithKeysToEmojiNames(message.value?.payload?.text),
data: transformTextWithKeysToEmojiNames(message.value?.payload?.text)
});
} else {
// uni-app conditional compilation will not run the following code
@ -295,17 +273,17 @@ function translateMessage() {
if (!enable) {
Toast({
message: TUITranslateService.t('TUIChat.请开通翻译功能'),
type: TOAST_TYPE.WARNING,
type: TOAST_TYPE.WARNING
});
return;
}
if (!message.value) return;
const index = actionItems.value.findIndex(item => item.key === 'translate');
const index = actionItems.value.findIndex((item) => item.key === 'translate');
TUIStore.update(StoreName.CHAT, 'translateTextInfo', {
conversationID: message.value.conversationID,
messageID: message.value.ID,
visible: !actionItems.value[index].visible,
visible: !actionItems.value[index].visible
});
}
@ -313,17 +291,17 @@ function convertVoiceToText() {
const enable = TUIStore.getData(StoreName.APP, 'enabledVoiceToText');
if (!enable) {
Toast({
message: TUITranslateService.t('TUIChat.请开通语音转文字功能'),
message: TUITranslateService.t('TUIChat.请开通语音转文字功能')
});
return;
}
if (!message.value) return;
const index = actionItems.value.findIndex(item => item.key === 'convert');
const index = actionItems.value.findIndex((item) => item.key === 'convert');
TUIStore.update(StoreName.CHAT, 'voiceToTextInfo', {
conversationID: message.value.conversationID,
messageID: message.value.ID,
visible: !actionItems.value[index].visible,
visible: !actionItems.value[index].visible
});
}
@ -334,7 +312,7 @@ function multipleSelectMessage() {
function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
if (info === undefined) return;
const translationInfoList = info.get(props.messageItem.conversationID) || [];
const idx = actionItems.value.findIndex(item => item.key === 'translate');
const idx = actionItems.value.findIndex((item) => item.key === 'translate');
for (let i = 0; i < translationInfoList.length; ++i) {
const { messageID, visible } = translationInfoList[i];
if (messageID === props.messageItem.ID) {
@ -349,7 +327,7 @@ function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
if (info === undefined) return;
const convertInfoList = info.get(props.messageItem.conversationID) || [];
const idx = actionItems.value.findIndex(item => item.key === 'convert');
const idx = actionItems.value.findIndex((item) => item.key === 'convert');
for (let i = 0; i < convertInfoList.length; ++i) {
const { messageID, visible } = convertInfoList[i];
if (messageID === props.messageItem.ID) {
@ -362,12 +340,12 @@ function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
}
defineExpose({
messageToolDom,
messageToolDom
});
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.dialog-item-web {
background: #fff;

View File

@ -1,14 +1,10 @@
<template>
<div class="revoke">
<span v-if="message.flow === 'in'">{{ message.nick || message.from }}</span>
<span v-else-if="message.from === message.revoker">{{ TUITranslateService.t("TUIChat.您") }}</span>
<span v-else-if="message.from === message.revoker">{{ TUITranslateService.t('TUIChat.您') }}</span>
<span v-else>{{ message.revoker }}</span>
<span>{{ TUITranslateService.t("TUIChat.撤回了一条消息") }}</span>
<span
v-if="message.flow === 'out' && isEditMsg"
class="edit"
@click="messageEdit"
>{{ TUITranslateService.t("TUIChat.重新编辑") }}</span>
<span>{{ TUITranslateService.t('TUIChat.撤回了一条消息') }}</span>
<span v-if="message.flow === 'out' && isEditMsg" class="edit" @click="messageEdit">{{ TUITranslateService.t('TUIChat.重新编辑') }}</span>
</div>
</template>
@ -18,12 +14,12 @@ import { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-eng
const props = defineProps({
isEdit: {
type: Boolean,
default: () => false,
default: () => false
},
messageItem: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const message = ref<IMessageModel>();
@ -39,7 +35,7 @@ const messageEdit = () => {
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
@import '../../../../assets/styles/common';
.revoke {
display: flex;

View File

@ -1,27 +1,19 @@
<template>
<Overlay
:maskColor="'transparent'"
@onOverlayClick="closeReadReceiptPanel"
>
<Overlay :maskColor="'transparent'" @onOverlayClick="closeReadReceiptPanel">
<div
:class="{
'read-receipt-panel': true,
'read-receipt-panel-mobile': isMobile,
'read-receipt-panel-uni': isUniFrameWork,
'read-receipt-panel-close-mobile': isMobile && isPanelClose,
'read-receipt-panel-close-mobile': isMobile && isPanelClose
}"
>
<div class="header">
<div class="header-text">
{{ TUITranslateService.t("TUIChat.消息详情") }}
{{ TUITranslateService.t('TUIChat.消息详情') }}
</div>
<div class="header-close-icon">
<Icon
size="12px"
hotAreaSize="8"
:file="closeIcon"
@onClick="closeReadReceiptPanel"
/>
<Icon size="12px" hotAreaSize="8" :file="closeIcon" @onClick="closeReadReceiptPanel" />
</div>
</div>
<div class="read-status-counter-container">
@ -30,7 +22,7 @@
:key="tabName"
:class="{
'read-status-counter': true,
'active': tabName === currentTabName,
'active': tabName === currentTabName
}"
@click="toggleTabName(tabName)"
>
@ -38,60 +30,34 @@
{{ tabInfo[tabName].tabName }}
</div>
<div class="status-count">
{{ tabInfo[tabName].count === undefined ? "" : tabInfo[tabName].count }}
{{ tabInfo[tabName].count === undefined ? '' : tabInfo[tabName].count }}
</div>
</div>
</div>
<div class="read-status-member-list">
<div
v-if="tabInfo[currentTabName].count === 0 && isFirstLoadFinished"
class="empty-list-tip"
>
<div v-if="tabInfo[currentTabName].count === 0 && isFirstLoadFinished" class="empty-list-tip">
- {{ TUITranslateService.t('TUIChat.空') }} -
</div>
<template v-else-if="isFirstLoadFinished">
<template v-if="currentTabName === 'unread'">
<div
v-for="item in tabInfo[currentTabName].memberList"
:key="item.userID"
class="read-status-member-container"
>
<Avatar
class="read-status-avatar"
useSkeletonAnimation
:url="item.avatar || ''"
/>
<div v-for="item in tabInfo[currentTabName].memberList" :key="item.userID" class="read-status-member-container">
<Avatar class="read-status-avatar" useSkeletonAnimation :url="item.avatar || ''" />
<div class="username">
{{ item.nick || item.userID }}
</div>
</div>
</template>
<template v-if="currentTabName === 'read'">
<div
v-for="item in tabInfo[currentTabName].memberList"
:key="item.userID"
class="read-status-member-container"
>
<Avatar
class="read-status-avatar"
useSkeletonAnimation
:url="item.avatar"
/>
<div v-for="item in tabInfo[currentTabName].memberList" :key="item.userID" class="read-status-member-container">
<Avatar class="read-status-avatar" useSkeletonAnimation :url="item.avatar" />
<div class="username">
{{ item.nick || item.userID }}
</div>
</div>
</template>
</template>
<div
v-if="isFirstLoadFinished"
class="fetch-more-container"
>
<FetchMore
:isFetching="isPullDownFetching"
:isTerminateObserve="isStopFetchMore"
@onExposed="pullDownFetchMoreData"
/>
<div v-if="isFirstLoadFinished" class="fetch-more-container">
<FetchMore :isFetching="isPullDownFetching" :isTerminateObserve="isStopFetchMore" @onExposed="pullDownFetchMoreData" />
</div>
</div>
</div>
@ -122,7 +88,7 @@ interface IEmits {
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
message: () => ({}) as IMessageModel,
message: () => ({}) as IMessageModel
});
let lastUnreadCursor: string = '';
@ -131,7 +97,7 @@ const tabNameList: TabName[] = ['unread', 'read'];
const isListFetchCompleted: Record<TabName, boolean> = {
unread: false,
read: false,
close: false,
close: false
};
const isPullDownFetching = ref<boolean>(false);
@ -152,7 +118,7 @@ watch(
() => props.message.readReceiptInfo.readCount,
() => {
initAndRefetchReceiptInfomation();
},
}
);
async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'all') {
@ -166,7 +132,7 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
message,
filter: 1,
cursor: lastUnreadCursor,
count: 100,
count: 100
});
if (unreadResult) {
lastUnreadCursor = unreadResult.data.cursor;
@ -181,7 +147,7 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
message,
filter: 0,
cursor: lastReadCursor,
count: 100,
count: 100
});
if (readResult) {
lastReadCursor = readResult.data.cursor;
@ -197,12 +163,12 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
return {
unreadResult: {
count: totalUnreadCount,
...unreadResult.data,
...unreadResult.data
},
readResult: {
count: totalReadCount,
...readResult.data,
},
...readResult.data
}
};
}
@ -281,18 +247,18 @@ function generateInitalTabInfo(): ITabInfo {
read: {
tabName: TUITranslateService.t('TUIChat.已读'),
count: undefined,
memberList: [],
memberList: []
},
unread: {
tabName: TUITranslateService.t('TUIChat.未读'),
count: undefined,
memberList: [],
memberList: []
},
close: {
tabName: TUITranslateService.t('TUIChat.关闭'),
count: undefined,
memberList: [],
},
memberList: []
}
};
}

View File

@ -1,14 +1,6 @@
<template>
<div
v-if="isScrollButtonVisible"
class="scroll-button"
@click="scrollToMessageListBottom"
>
<Icon
width="10px"
height="10px"
:file="doubleArrowIcon"
/>
<div v-if="isScrollButtonVisible" class="scroll-button" @click="scrollToMessageListBottom">
<Icon width="10px" height="10px" :file="doubleArrowIcon" />
<div class="scroll-button-text">
{{ scrollButtonContent }}
</div>
@ -17,13 +9,7 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed, watch } from '../../../../adapter-vue';
import {
TUIStore,
StoreName,
IMessageModel,
IConversationModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName, IMessageModel, IConversationModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue';
import doubleArrowIcon from '../../../../assets/icon/double-arrow.svg';
import { getBoundingClientRect } from '@tencentcloud/universal-api';
@ -42,36 +28,37 @@ const isScrollOverOneScreen = ref<boolean>(false);
const isExistLastMessage = ref<boolean>(false);
const isScrollButtonVisible = ref<boolean>(false);
const scrollButtonContent = computed(() =>
newMessageCount.value ? `${newMessageCount.value}${TUITranslateService.t('TUIChat.条新消息')}` : TUITranslateService.t('TUIChat.回到最新位置'),
newMessageCount.value ? `${newMessageCount.value}${TUITranslateService.t('TUIChat.条新消息')}` : TUITranslateService.t('TUIChat.回到最新位置')
);
watch(() => [isScrollOverOneScreen.value, isExistLastMessage.value],
watch(
() => [isScrollOverOneScreen.value, isExistLastMessage.value],
() => {
isScrollButtonVisible.value = isScrollOverOneScreen.value || isExistLastMessage.value;
if (!isScrollButtonVisible.value) {
resetNewMessageCount();
}
},
{ immediate: true },
{ immediate: true }
);
onMounted(() => {
TUIStore.watch(StoreName.CHAT, {
messageList: onMessageListUpdated,
newMessageList: onNewMessageListUpdated,
newMessageList: onNewMessageListUpdated
});
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, {
messageList: onMessageListUpdated,
newMessageList: onNewMessageListUpdated,
newMessageList: onNewMessageListUpdated
});
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
});
@ -82,15 +69,19 @@ function isTypingMessage(message: IMessageModel): boolean {
function onMessageListUpdated(newMessageList: IMessageModel[]) {
messageList.value = newMessageList || [];
const lastMessage = messageList.value?.[messageList.value?.length - 1];
isExistLastMessage.value = !!(
lastMessage && lastMessage?.time < currentLastMessageTime?.value
);
isExistLastMessage.value = !!(lastMessage && lastMessage?.time < currentLastMessageTime?.value);
}
function onNewMessageListUpdated(newMessageList: IMessageModel[]) {
if (Array.isArray(newMessageList) && isScrollButtonVisible.value) {
newMessageList.forEach((message: IMessageModel) => {
if (message && message.conversationID === currentConversationID.value && !message.isDeleted && !message.isRevoked && !isTypingMessage(message)) {
if (
message &&
message.conversationID === currentConversationID.value &&
!message.isDeleted &&
!message.isRevoked &&
!isTypingMessage(message)
) {
newMessageCount.value += 1;
}
});
@ -109,7 +100,7 @@ function onCurrentConversationUpdated(conversation: IConversationModel | undefin
async function judgeScrollOverOneScreen(e: Event) {
if (e.target) {
try {
const { height } = await getBoundingClientRect(`#${(e.target as HTMLElement)?.id}`, 'messageList') || {};
const { height } = (await getBoundingClientRect(`#${(e.target as HTMLElement)?.id}`, 'messageList')) || {};
const scrollHeight = (e.target as HTMLElement)?.scrollHeight || (e.detail as HTMLElement)?.scrollHeight;
const scrollTop = (e.target as HTMLElement)?.scrollTop || (e.detail as HTMLElement)?.scrollTop || 0;
// while scroll over one screen show this scroll button.
@ -144,7 +135,7 @@ function scrollToMessageListBottom() {
defineExpose({
judgeScrollOverOneScreen,
isScrollButtonVisible,
isScrollButtonVisible
});
</script>

View File

@ -2,52 +2,35 @@
<div
:class="{
'mulitple-select-panel': true,
'mulitple-select-panel-mobile': isMobile,
'mulitple-select-panel-mobile': isMobile
}"
>
<div
class="forward-button"
@click="oneByOneForwardMessage"
>
<Icon
:file="ForwardEachIcon"
:size="iconSize"
/>
<div class="forward-button" @click="oneByOneForwardMessage">
<Icon :file="ForwardEachIcon" :size="iconSize" />
<span
:class="{
'forward-button-text': true,
'forward-button-text-mobile': isMobile,
'forward-button-text-mobile': isMobile
}"
>{{ TUITranslateService.t('TUIChat.逐条转发') }}</span>
>{{ TUITranslateService.t('TUIChat.逐条转发') }}</span
>
</div>
<div
class="forward-button"
@click="mergeForwardMessage"
>
<Icon
:file="ForwardMergeIcon"
:size="iconSize"
/>
<div class="forward-button" @click="mergeForwardMessage">
<Icon :file="ForwardMergeIcon" :size="iconSize" />
<span
:class="{
'forward-button-text': true,
'forward-button-text-mobile': isMobile,
'forward-button-text-mobile': isMobile
}"
>{{ TUITranslateService.t('TUIChat.合并转发') }}</span>
</div>
<div
class="forward-button"
@click="cancelMultipleSelect"
>{{ TUITranslateService.t('TUIChat.合并转发') }}</span
>
<Icon
class="cancel-button-icon"
:file="AddIcon"
:size="iconSize"
/>
</div>
<div class="forward-button" @click="cancelMultipleSelect">
<Icon class="cancel-button-icon" :file="AddIcon" :size="iconSize" />
<span
:class="{
'forward-button-text': true,
'forward-button-text-mobile': isMobile,
'forward-button-text-mobile': isMobile
}"
>
{{ TUITranslateService.t('TUIChat.取消') }}
@ -58,9 +41,7 @@
<script lang="ts" setup>
import { ref } from '../../../adapter-vue';
import {
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../common/Icon.vue';
import ForwardEachIcon from '../../../assets/icon/forward-each.svg';
import ForwardMergeIcon from '../../../assets/icon/forward-merge.svg';
@ -104,7 +85,7 @@ function cancelMultipleSelect() {
flex-direction: row;
justify-content: space-around;
align-items: center;
background-color: #EBF0F6;
background-color: #ebf0f6;
&-mobile {
height: 64px;

View File

@ -8,10 +8,10 @@ export const DEFAULT_DESC: any = {
[TUIChatEngine.TYPES.MSG_VIDEO]: '[视频]',
[TUIChatEngine.TYPES.MSG_LOCATION]: '[地理位置]',
[TUIChatEngine.TYPES.MSG_MERGER]: '[聊天记录]',
[TUIChatEngine.TYPES.MSG_CUSTOM]: '[自定义消息]',
[TUIChatEngine.TYPES.MSG_CUSTOM]: '[自定义消息]'
};
export enum PUSH_SCENE {
CHAT = 'chat',
CALL = 'call',
CALL = 'call'
}

View File

@ -2,7 +2,7 @@ import { IChatOfflinePushInfo, ICallOfflinePushInfo } from './interface';
export const chatOfflinePushInfo: IChatOfflinePushInfo = {
androidInfo: {},
apnsInfo: {},
apnsInfo: {}
};
export const callOfflinePushInfo: ICallOfflinePushInfo = {};

View File

@ -1,9 +1,6 @@
import TUIChatEngine, { IConversationModel, StoreName, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { transformTextWithKeysToEmojiNames } from '../emoji-config';
import {
IChatOfflinePushInfo,
IOfflinePushInfoCreateParams,
} from './interface';
import { IChatOfflinePushInfo, IOfflinePushInfoCreateParams } from './interface';
import { chatOfflinePushInfo, callOfflinePushInfo } from './info';
import { DEFAULT_DESC, PUSH_SCENE } from './const';
@ -16,7 +13,7 @@ class OfflinePushInfoManager {
private constructor() {
this.offlinePushInfo = {
[PUSH_SCENE.CHAT]: chatOfflinePushInfo,
[PUSH_SCENE.CALL]: callOfflinePushInfo,
[PUSH_SCENE.CALL]: callOfflinePushInfo
};
}
@ -62,13 +59,13 @@ class OfflinePushInfoManager {
nickName: userInfo?.nick,
chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1,
version: 1,
action: 1,
action: 1
};
return {
title: this.genTitle(conversation, userInfo),
description: this.genDesc(messageType, payload),
extension: JSON.stringify({ entity }),
...this.offlinePushInfo[PUSH_SCENE.CHAT],
...this.offlinePushInfo[PUSH_SCENE.CHAT]
};
}
}

View File

@ -1,10 +1,5 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import {
IMessageModel,
TUIStore,
StoreName,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import { IMessageModel, TUIStore, StoreName, TUIChatService } from '@tencentcloud/chat-uikit-engine';
import TUIChatConfig from './config';
export default class TUIChatServer {
@ -21,7 +16,7 @@ export default class TUIChatServer {
TUIStore.watch(StoreName.CONV, {
currentConversationID: (id: string) => {
this.currentConversationID = id;
},
}
});
}

View File

@ -4,7 +4,7 @@ import {
StoreName,
TUIConversationService,
TUIStore,
TUITranslateService,
TUITranslateService
} from '@tencentcloud/chat-uikit-engine';
import { transformTextWithKeysToEmojiNames } from '../emoji-config';
import { JSONToObject } from '../../../utils/index';
@ -13,7 +13,7 @@ class ConversationDraftManager {
private static instance: ConversationDraftManager | null = null;
private quoteMessageMap = new Map<string, IMessageModel>();
private constructor() { }
private constructor() {}
public static getInstance(): ConversationDraftManager {
if (!ConversationDraftManager.instance) {
@ -22,7 +22,12 @@ class ConversationDraftManager {
return ConversationDraftManager.instance;
}
public setStore(conversationID: string, draftContent: string, abstract: string, quoteMessage?: { type: 'quote' | 'reply'; message: IMessageModel }) {
public setStore(
conversationID: string,
draftContent: string,
abstract: string,
quoteMessage?: { type: 'quote' | 'reply'; message: IMessageModel }
) {
if (conversationID && (this.isEditorNotEmpty(draftContent) || quoteMessage?.message?.ID)) {
let additionalDraftInfo = {};
if (quoteMessage?.message?.ID) {
@ -34,8 +39,8 @@ class ConversationDraftManager {
draftInfo: {
html: draftContent,
abstract: abstract,
...additionalDraftInfo,
},
...additionalDraftInfo
}
};
TUIConversationService.setConversationDraft(draftParams);
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: undefined, type: 'quote' });
@ -49,7 +54,10 @@ class ConversationDraftManager {
}
if (conversation.conversationID && conversation.draftText) {
const draftObject = JSONToObject(conversation.draftText);
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: this.quoteMessageMap.get(draftObject.messageID) || undefined, type: draftObject.type });
TUIStore.update(StoreName.CHAT, 'quoteMessage', {
message: this.quoteMessageMap.get(draftObject.messageID) || undefined,
type: draftObject.type
});
setEditorContentCallback(draftObject.html);
}
TUIConversationService.setConversationDraft({ conversationID: conversation.conversationID });

View File

@ -1,8 +1,4 @@
import {
IMessageModel,
TUIChatService,
TUIStore,
} from '@tencentcloud/chat-uikit-engine';
import { IMessageModel, TUIChatService, TUIStore } from '@tencentcloud/chat-uikit-engine';
import { IChatResponese } from '../../../interface';
class Convertor {
@ -35,7 +31,7 @@ class Convertor {
}
// step3: get response from api
const response: IChatResponese<{ result: string }> = await TUIChatService.convertVoiceToText({
message: currentMessage,
message: currentMessage
});
let { data: { result } = {} } = response;
if (result) {

View File

@ -33,7 +33,7 @@ class CopyManager {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset,
focusOffset: selection.focusOffset
};
}
@ -49,9 +49,8 @@ class CopyManager {
}
const range = document.createRange();
const isForwardSelection = anchorNode === focusNode
? anchorOffset <= focusOffset
: anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
const isForwardSelection =
anchorNode === focusNode ? anchorOffset <= focusOffset : anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
if (isForwardSelection) {
range.setStart(anchorNode, anchorOffset);
range.setEnd(focusNode, focusOffset);
@ -88,7 +87,7 @@ class CopyManager {
}
public async copyTextOrHtml(content: string, type: 'text' | 'html'): Promise<void> {
const mimeType = (type === 'html') ? 'text/html' : 'text/plain';
const mimeType = type === 'html' ? 'text/html' : 'text/plain';
// prefer use clipboard api to copy node or text
if (navigator.clipboard) {
try {
@ -97,7 +96,7 @@ class CopyManager {
await navigator.clipboard.write([clipboardItem]);
Toast({
message: TUITranslateService.t('TUIChat.复制成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
return;
} catch (err) {
@ -132,12 +131,12 @@ class CopyManager {
document.execCommand('copy');
Toast({
message: TUITranslateService.t('TUIChat.复制成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
} catch (err) {
Toast({
message: TUITranslateService.t('TUIChat.此机型暂不支持复制'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
console.warn('use document.execCommand copy failed:', err);
} finally {

View File

@ -4,7 +4,7 @@ import TUIChatEngine, {
StoreName,
TUITranslateService,
IConversationModel,
SendMessageParams,
SendMessageParams
} from '@tencentcloud/chat-uikit-engine';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import { isEnabledMessageReadReceiptGlobal } from '../utils/utils';
@ -21,7 +21,7 @@ export const sendMessageErrorCodeMap: Map<number, string> = new Map([
[8001, '消息长度超出限制,消息长度不要超过12K'],
[80001, '消息或者资料中文本存在敏感内容,发送失败'],
[80004, '消息中图片存在敏感内容,发送失败'],
[10017, '您已被禁止聊天'],
[10017, '您已被禁止聊天']
]);
export const createOfflinePushInfo = (conversation: IConversationModel) => {
@ -33,12 +33,12 @@ export const createOfflinePushInfo = (conversation: IConversationModel) => {
nickName: userInfo.nick,
chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1,
version: 1,
action: 1,
action: 1
};
return {
extension: JSON.stringify({ entity }),
androidInfo,
apnsInfo,
apnsInfo
};
};
@ -47,10 +47,7 @@ export const createOfflinePushInfo = (conversation: IConversationModel) => {
* @param messageList
* @param currentConversation
*/
export const sendMessages = async (
messageList: ITipTapEditorContent[],
currentConversation: IConversationModel,
) => {
export const sendMessages = async (messageList: ITipTapEditorContent[], currentConversation: IConversationModel) => {
// In case of messageJumping, the sent message is automatically cleared and returns to the bottom
if (TUIStore.getData(StoreName.CHAT, 'messageSource')) {
TUIStore.update(StoreName.CHAT, 'messageSource', undefined);
@ -61,17 +58,17 @@ export const sendMessages = async (
to: currentConversation?.groupProfile?.groupID || currentConversation?.userProfile?.userID,
conversationType: currentConversation?.type as any,
payload: {},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
needReadReceipt: isEnabledMessageReadReceiptGlobal()
};
// handle message typing
let textMessageContent;
const sendMessageOptions = {
offlinePushInfo: {},
offlinePushInfo: {}
};
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation,
payload: content.payload,
messageType: '',
messageType: ''
};
switch (content?.type) {
case 'text':
@ -81,7 +78,7 @@ export const sendMessages = async (
break;
}
options.payload = {
text: textMessageContent,
text: textMessageContent
};
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_TEXT;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -94,7 +91,7 @@ export const sendMessages = async (
break;
case 'image':
options.payload = {
file: content.payload?.file,
file: content.payload?.file
};
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_IMAGE;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -102,7 +99,7 @@ export const sendMessages = async (
break;
case 'video':
options.payload = {
file: content.payload?.file,
file: content.payload?.file
};
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_VIDEO;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -110,7 +107,7 @@ export const sendMessages = async (
break;
case 'file':
options.payload = {
file: content.payload?.file,
file: content.payload?.file
};
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_FILE;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -125,7 +122,7 @@ export const sendMessages = async (
message: sendMessageErrorCodeMap.get(error?.code)
? TUITranslateService.t(`TUIChat.${sendMessageErrorCodeMap.get(error.code) as string}`)
: error?.message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
// If the message fails to be sent and the message is a reference message, clear the reference message information
if (TUIStore.getData(StoreName.CHAT, 'quoteMessage')) {
@ -141,7 +138,7 @@ export const handleMessageWithTyping = (cloudCustomData: any) => {
}
cloudCustomData.messageFeature = {
needTyping: 1,
version: 1,
version: 1
};
return cloudCustomData;
};

View File

@ -1,10 +1,4 @@
import TUIChatEngine, {
IMessageModel,
TUIChatService,
TUIStore,
TUITranslateService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { IMessageModel, TUIChatService, TUIStore, TUITranslateService, TUIUserService } from '@tencentcloud/chat-uikit-engine';
import { IChatResponese, IUserProfile } from '../../../interface';
/**
@ -80,20 +74,22 @@ class Translator {
// step4: filter plain text to be translated
const needTranslateTextIndex: number[] = [];
const needTranslateText = textList.filter((item, index) => {
const needTranslateText = textList
.filter((item, index) => {
if (item.type === 'text' && item.value.trim() !== '') {
needTranslateTextIndex.push(index);
return true;
}
return false;
}).map(item => item.value);
})
.map((item) => item.value);
if (needTranslateText.length === 0) {
this.translationCache.set(currentMessage.ID, textList);
return textList;
}
// step5: get final translation result
const translationResult = await this.getTranslationStandard(needTranslateText) as string[];
const translationResult = (await this.getTranslationStandard(needTranslateText)) as string[];
translationResult.forEach((item, index) => {
textList[needTranslateTextIndex[index]].value = item;
});
@ -122,11 +118,11 @@ class Translator {
return new Promise((resolve, reject) => {
TUIChatService.translateText({
sourceTextList: originTextList,
sourceLanguage: 'auto',
sourceLanguage: 'auto'
})
.then((response: IChatResponese<{ translatedTextList: string[] }>) => {
const {
data: { translatedTextList },
data: { translatedTextList }
} = response;
resolve(translatedTextList);
})
@ -149,7 +145,7 @@ class Translator {
splittingList.push(`@${TUITranslateService.t('TUIChat.所有人')}`);
}
if (atUserList.length > 0) {
const { data: userProfileList } = await TUIUserService.getUserProfile({ userIDList: atUserList }) as IChatResponese<IUserProfile[]>;
const { data: userProfileList } = (await TUIUserService.getUserProfile({ userIDList: atUserList })) as IChatResponese<IUserProfile[]>;
userProfileList.forEach((user) => {
const atNick = `@${user.nick || user.userID}`;
splittingList.push(atNick);
@ -195,7 +191,7 @@ class Translator {
}
return {
transSplitingList,
atNickList,
atNickList
};
}
}

View File

@ -27,21 +27,13 @@ export function deepCopy(data: any, hash = new WeakMap()) {
return newData;
}
export const handleSkeletonSize = (
width: number,
height: number,
maxWidth: number,
maxHeight: number,
): { width: number; height: number } => {
export const handleSkeletonSize = (width: number, height: number, maxWidth: number, maxHeight: number): { width: number; height: number } => {
const widthToHeight = width / height;
const maxWidthToHeight = maxWidth / maxHeight;
if (width <= maxWidth && height <= maxHeight) {
return { width, height };
}
if (
(width <= maxWidth && height > maxHeight)
|| (width > maxWidth && height > maxHeight && widthToHeight <= maxWidthToHeight)
) {
if ((width <= maxWidth && height > maxHeight) || (width > maxWidth && height > maxHeight && widthToHeight <= maxWidthToHeight)) {
return { width: width * (maxHeight / height), height: maxHeight };
}
return { width: maxWidth, height: height * (maxWidth / width) };
@ -76,10 +68,7 @@ export function getImgLoad(container: any, className: string, callback: any) {
}
export const isCreateGroupCustomMessage = (message: IMessageModel) => {
return (
message.type === TUIChatEngine.TYPES.MSG_CUSTOM
&& message?.getMessageContent()?.businessID === 'group_create'
);
return message.type === TUIChatEngine.TYPES.MSG_CUSTOM && message?.getMessageContent()?.businessID === 'group_create';
};
/**
@ -91,8 +80,7 @@ export const isCreateGroupCustomMessage = (message: IMessageModel) => {
* @return {boolean} - Returns a boolean value indicating if the message read receipt is enabled globally.
*/
export function isEnabledMessageReadReceiptGlobal(): boolean {
return TUIStore.getData(StoreName.USER, 'displayMessageReadReceipt')
&& TUIStore.getData(StoreName.APP, 'enabledMessageReadReceipt');
return TUIStore.getData(StoreName.USER, 'displayMessageReadReceipt') && TUIStore.getData(StoreName.APP, 'enabledMessageReadReceipt');
}
export function shallowCopyMessage(message: IMessageModel) {
@ -102,15 +90,7 @@ export function shallowCopyMessage(message: IMessageModel) {
// calculate timestamp
export function calculateTimestamp(timestamp: number): string {
const todayZero = new Date().setHours(0, 0, 0, 0);
const thisYear = new Date(
new Date().getFullYear(),
0,
1,
0,
0,
0,
0,
).getTime();
const thisYear = new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTime();
const target = new Date(timestamp);
const oneDay = 24 * 60 * 60 * 1000;
@ -127,35 +107,17 @@ export function calculateTimestamp(timestamp: number): string {
return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
} else if (diff <= oneDay) {
// yesterday, display yesterday:hour:minute
return `${TUITranslateService.t('time.昨天')} ${formatNum(
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
return `${TUITranslateService.t('time.昨天')} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
} else if (diff <= oneWeek - oneDay) {
// Within a week, display weekday hour:minute
const weekdays = [
'星期日',
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
];
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const weekday = weekdays[target.getDay()];
return `${TUITranslateService.t('time.' + weekday)} ${formatNum(
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
return `${TUITranslateService.t('time.' + weekday)} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
} else if (target.getTime() >= thisYear) {
// Over a week, within this year, display mouth/day hour:minute
return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
} else {
// Not within this year, display year/mouth/day hour:minute
return `${target.getFullYear()}/${
target.getMonth() + 1
}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(
target.getMinutes(),
)}`;
return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
}
}

View File

@ -1,29 +1,29 @@
export const wordsList = [
{
value: '在吗?在吗?在吗?重要的话说三遍。',
value: '在吗?在吗?在吗?重要的话说三遍。'
},
{
value: '好久没聊天了,快来和我说说话~',
value: '好久没聊天了,快来和我说说话~'
},
{
value: '好的,就这么说定了。',
value: '好的,就这么说定了。'
},
{
value: '感恩的心,感谢有你。',
value: '感恩的心,感谢有你。'
},
{
value: '糟糕!是心动的感觉!',
value: '糟糕!是心动的感觉!'
},
{
value: '心疼地抱抱自己,我太难了!',
value: '心疼地抱抱自己,我太难了!'
},
{
value: '没关系,别在意,事情过去就过去了。',
value: '没关系,别在意,事情过去就过去了。'
},
{
value: '早上好,今天也是让人期待的一天呢!',
value: '早上好,今天也是让人期待的一天呢!'
},
{
value: '熬夜有什么用,又没人陪你聊天,早点休息吧。',
},
value: '熬夜有什么用,又没人陪你聊天,早点休息吧。'
}
];

View File

@ -1,9 +1,5 @@
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
} from '../../../constant';
import { CONTACT_INFO_LABEL_POSITION, CONTACT_INFO_MORE_EDIT_TYPE, CONTACT_INFO_BUTTON_TYPE } from '../../../constant';
import {
updateFriendRemark,
deleteFriend,
@ -15,7 +11,7 @@ import {
acceptFriendApplication,
refuseFriendApplication,
addToBlacklist,
removeFromBlacklist,
removeFromBlacklist
} from '../utils/index';
export const contactMoreInfoConfig = {
@ -28,11 +24,7 @@ export const contactMoreInfoConfig = {
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.INPUT,
editing: false,
editSubmitHandler: (props: {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
editSubmitHandler: (props: { item: any; contactInfoData: any; [propsName: string]: any }) => {
if (props?.isBothFriend) {
const newRemarkValue = props?.item?.data;
updateFriendRemark(props?.contactInfoData?.userID, newRemarkValue);
@ -41,7 +33,7 @@ export const contactMoreInfoConfig = {
} else {
props?.item?.editing && (props.item.editing = false);
}
},
}
},
// blocked list
blackList: {
@ -52,18 +44,14 @@ export const contactMoreInfoConfig = {
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.SWITCH,
editing: true,
editSubmitHandler: (props: {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
editSubmitHandler: (props: { item: any; contactInfoData: any; [propsName: string]: any }) => {
if (props?.isInBlackList) {
removeFromBlacklist(props?.contactInfoData?.userID);
} else {
addToBlacklist(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'blackList');
}
},
}
},
// Fill in verification words (applicant)
setWords: {
@ -73,7 +61,7 @@ export const contactMoreInfoConfig = {
labelPosition: CONTACT_INFO_LABEL_POSITION.TOP,
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA,
editing: true,
editing: true
},
// Display verification words (application recipient)
displayWords: {
@ -81,8 +69,8 @@ export const contactMoreInfoConfig = {
label: '验证信息',
data: '',
labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT,
editable: false,
},
editable: false
}
};
export const contactButtonConfig = {
@ -95,7 +83,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
dismissGroup(props?.contactInfoData?.groupID);
},
}
},
quitGroup: {
key: 'quitGroup',
@ -103,34 +91,23 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
quitGroup(props?.contactInfoData?.groupID);
},
}
},
joinGroup: {
key: 'joinGroup',
label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
joinGroup(
props?.contactInfoData?.groupID,
props?.contactInfoMoreList[0]?.data,
);
},
onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
joinGroup(props?.contactInfoData?.groupID, props?.contactInfoMoreList[0]?.data);
}
},
joinAVChatGroup: {
key: 'joinAVChatGroup',
label: '加入直播群',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
joinGroup(props?.contactInfoData?.groupID);
},
}
},
enterGroupConversation: {
key: 'enterGroupConversation',
@ -138,7 +115,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData);
},
}
},
// ---------------------
@ -148,18 +125,14 @@ export const contactButtonConfig = {
key: 'addFriend',
label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
addFriend({
to: props?.contactInfoData?.userID,
source: 'AddSource_Type_Web',
remark: props?.contactInfoMoreList[1]?.data,
wording: props?.contactInfoMoreList[0]?.data,
wording: props?.contactInfoMoreList[0]?.data
});
},
}
},
deleteFriend: {
key: 'deleteFriend',
@ -167,7 +140,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
deleteFriend(props?.contactInfoData?.userID);
},
}
},
enterC2CConversation: {
key: 'enterC2CConversation',
@ -175,7 +148,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData);
},
}
},
// ---------------------
@ -188,7 +161,7 @@ export const contactButtonConfig = {
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
acceptFriendApplication(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'friendList');
},
}
},
refuseFriendApplication: {
key: 'refuseFriendApplication',
@ -196,6 +169,6 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
refuseFriendApplication(props?.contactInfoData?.userID);
},
},
}
}
};

View File

@ -3,53 +3,23 @@
v-if="typeof contactInfoData === 'object' && Object.keys(contactInfoData).length"
:class="['tui-contact-info', !isPC && 'tui-contact-info-h5']"
>
<div
v-if="!isPC"
:class="[
'tui-contact-info-header',
!isPC && 'tui-contact-info-h5-header',
]"
>
<div
:class="[
'tui-contact-info-header-icon',
!isPC && 'tui-contact-info-h5-header-icon',
]"
@click="resetContactSearchingUIData"
>
<div v-if="!isPC" :class="['tui-contact-info-header', !isPC && 'tui-contact-info-h5-header']">
<div :class="['tui-contact-info-header-icon', !isPC && 'tui-contact-info-h5-header-icon']" @click="resetContactSearchingUIData">
<Icon :file="backSVG" />
</div>
<div
:class="[
'tui-contact-info-header-title',
!isPC && 'tui-contact-info-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
<div :class="['tui-contact-info-header-title', !isPC && 'tui-contact-info-h5-header-title']">
{{ TUITranslateService.t('TUIContact.添加好友/群聊') }}
</div>
</div>
<div :class="['tui-contact-info-basic', !isPC && 'tui-contact-info-h5-basic']">
<div
:class="[
'tui-contact-info-basic-text',
!isPC && 'tui-contact-info-h5-basic-text',
]"
>
<div
:class="[
'tui-contact-info-basic-text-name',
!isPC && 'tui-contact-info-h5-basic-text-name',
]"
>
<div :class="['tui-contact-info-basic-text', !isPC && 'tui-contact-info-h5-basic-text']">
<div :class="['tui-contact-info-basic-text-name', !isPC && 'tui-contact-info-h5-basic-text-name']">
{{ generateContactInfoName(contactInfoData) }}
</div>
<div
v-for="item in contactInfoBasicList"
:key="item.label"
:class="[
'tui-contact-info-basic-text-other',
!isPC && 'tui-contact-info-h5-basic-text-other',
]"
:class="['tui-contact-info-basic-text-other', !isPC && 'tui-contact-info-h5-basic-text-other']"
>
{{
`${TUITranslateService.t(`TUIContact.${item.label}`)}:
@ -57,117 +27,62 @@
}}
</div>
</div>
<img
:class="[
'tui-contact-info-basic-avatar',
!isPC && 'tui-contact-info-h5-basic-avatar',
]"
:src="generateAvatar(contactInfoData)"
>
<img :class="['tui-contact-info-basic-avatar', !isPC && 'tui-contact-info-h5-basic-avatar']" :src="generateAvatar(contactInfoData)" />
</div>
<div
v-if="contactInfoMoreList[0]"
:class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']"
>
<div v-if="contactInfoMoreList[0]" :class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']">
<div
v-for="item in contactInfoMoreList"
:key="item.key"
:class="[
'tui-contact-info-more-item',
!isPC && 'tui-contact-info-h5-more-item',
item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP
? 'tui-contact-info-more-item-top'
: 'tui-contact-info-more-item-left',
]"
>
<div
:class="[
'tui-contact-info-more-item-label',
!isPC && 'tui-contact-info-h5-more-item-label',
item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP ? 'tui-contact-info-more-item-top' : 'tui-contact-info-more-item-left'
]"
>
<div :class="['tui-contact-info-more-item-label', !isPC && 'tui-contact-info-h5-more-item-label']">
{{ `${TUITranslateService.t(`TUIContact.${item.label}`)}` }}
</div>
<div
:class="[
'tui-contact-info-more-item-content',
!isPC && 'tui-contact-info-h5-more-item-content',
]"
>
<div
v-if="!item.editing"
:class="[
'tui-contact-info-more-item-content-text',
!isPC && 'tui-contact-info-h5-more-item-content-text',
]"
>
<div
:class="[
'tui-contact-info-more-item-content-text-data',
!isPC && 'tui-contact-info-h5-more-item-content-text-data',
]"
>
<div :class="['tui-contact-info-more-item-content', !isPC && 'tui-contact-info-h5-more-item-content']">
<div v-if="!item.editing" :class="['tui-contact-info-more-item-content-text', !isPC && 'tui-contact-info-h5-more-item-content-text']">
<div :class="['tui-contact-info-more-item-content-text-data', !isPC && 'tui-contact-info-h5-more-item-content-text-data']">
{{ item.data }}
</div>
<div
v-if="item.editable"
:class="[
'tui-contact-info-more-item-content-text-icon',
!isPC && 'tui-contact-info-h5-more-item-content-text-icon',
]"
:class="['tui-contact-info-more-item-content-text-icon', !isPC && 'tui-contact-info-h5-more-item-content-text-icon']"
@click="setEditing(item)"
>
<Icon
:file="editSVG"
width="14px"
height="14px"
/>
<Icon :file="editSVG" width="14px" height="14px" />
</div>
</div>
<input
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.INPUT"
v-model="item.data"
:class="[
'tui-contact-info-more-item-content-input',
!isPC && 'tui-contact-info-h5-more-item-content-input',
]"
:class="['tui-contact-info-more-item-content-input', !isPC && 'tui-contact-info-h5-more-item-content-input']"
type="text"
@confirm="onContactInfoEmitSubmit(item)"
@keyup.enter="onContactInfoEmitSubmit(item)"
>
/>
<textarea
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA"
v-model="item.data"
:class="[
'tui-contact-info-more-item-content-textarea',
!isPC && 'tui-contact-info-h5-more-item-content-textarea',
]"
:class="['tui-contact-info-more-item-content-textarea', !isPC && 'tui-contact-info-h5-more-item-content-textarea']"
confirm-type="done"
/>
<div
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH"
@click="onContactInfoEmitSubmit(item)"
>
<div v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH" @click="onContactInfoEmitSubmit(item)">
<SwitchBar :value="item.data" />
</div>
</div>
</div>
</div>
<div
:class="[
'tui-contact-info-button',
!isPC && 'tui-contact-info-h5-button',
]"
>
<div :class="['tui-contact-info-button', !isPC && 'tui-contact-info-h5-button']">
<button
v-for="item in contactInfoButtonList"
:key="item.key"
:class="[
'tui-contact-info-button-item',
!isPC && 'tui-contact-info-h5-button-item',
item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL
? `tui-contact-info-button-item-cancel`
: `tui-contact-info-button-item-submit`,
item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL ? `tui-contact-info-button-item-cancel` : `tui-contact-info-button-item-submit`
]"
@click="onContactInfoButtonClicked(item)"
>
@ -177,43 +92,19 @@
</div>
</template>
<script setup lang="ts">
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IGroupModel,
Friend,
FriendApplication,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IGroupModel, Friend, FriendApplication } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue';
import { isPC } from '../../../utils/env';
import {
generateAvatar,
generateContactInfoName,
generateContactInfoBasic,
isFriend,
isApplicationType,
} from '../utils/index';
import {
contactMoreInfoConfig,
contactButtonConfig,
} from './contact-info-config';
import { generateAvatar, generateContactInfoName, generateContactInfoBasic, isFriend, isApplicationType } from '../utils/index';
import { contactMoreInfoConfig, contactButtonConfig } from './contact-info-config';
import Icon from '../../common/Icon.vue';
import editSVG from '../../../assets/icon/edit.svg';
import backSVG from '../../../assets/icon/back.svg';
import SwitchBar from '../../common/SwitchBar/index.vue';
import {
IBlackListUserItem,
IContactInfoMoreItem,
IContactInfoButton,
} from '../../../interface';
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
} from '../../../constant';
import { IBlackListUserItem, IContactInfoMoreItem, IContactInfoButton } from '../../../interface';
import { CONTACT_INFO_LABEL_POSITION, CONTACT_INFO_MORE_EDIT_TYPE, CONTACT_INFO_BUTTON_TYPE } from '../../../constant';
import { deepCopy } from '../../TUIChat/utils/utils';
type IContactInfoType = IGroupModel | Friend | FriendApplication | IBlackListUserItem;
@ -229,9 +120,7 @@ const setEditing = (item: any) => {
item.editing = true;
};
const isGroup = computed((): boolean =>
(contactInfoData.value as IGroupModel)?.groupID ? true : false,
);
const isGroup = computed((): boolean => ((contactInfoData.value as IGroupModel)?.groupID ? true : false));
const isApplication = computed((): boolean => {
return isApplicationType(contactInfoData?.value);
@ -248,11 +137,8 @@ const isGroupMember = computed((): boolean => {
// is in black list, if is group type always false
const isInBlackList = computed((): boolean => {
return (
!isGroup.value
&& blackList.value?.findIndex(
(item: IBlackListUserItem) =>
item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID,
) >= 0
!isGroup.value &&
blackList.value?.findIndex((item: IBlackListUserItem) => item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID) >= 0
);
});
@ -260,19 +146,19 @@ const blackList = ref<IBlackListUserItem[]>([]);
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactInfo: onCurrentContactInfoUpdated
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
userBlacklist: onUserBlacklistUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactInfo: onCurrentContactInfoUpdated
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
userBlacklist: onUserBlacklistUpdated
});
});
@ -290,25 +176,22 @@ const resetContactSearchingUIData = () => {
};
const onContactInfoEmitSubmit = (item: any) => {
item.editSubmitHandler
&& item.editSubmitHandler({
item.editSubmitHandler &&
item.editSubmitHandler({
item,
contactInfoData: contactInfoData.value,
isBothFriend: isBothFriend.value,
isInBlackList: isInBlackList.value,
isInBlackList: isInBlackList.value
});
};
const onContactInfoButtonClicked = (item: any) => {
item.onClick
&& item.onClick({
item.onClick &&
item.onClick({
contactInfoData: contactInfoData.value,
contactInfoMoreList: contactInfoMoreList.value,
contactInfoMoreList: contactInfoMoreList.value
});
if (
item.key === 'enterGroupConversation'
|| item.key === 'enterC2CConversation'
) {
if (item.key === 'enterGroupConversation' || item.key === 'enterC2CConversation') {
emits('switchConversation', contactInfoData.value);
resetContactSearchingUIData();
}
@ -317,17 +200,14 @@ const onContactInfoButtonClicked = (item: any) => {
const generateMoreInfo = async () => {
if (!isApplication.value) {
if (
(!isGroup.value && !isBothFriend.value && !isInBlackList.value)
|| (isGroup.value
&& !isGroupMember.value
&& (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
(!isGroup.value && !isBothFriend.value && !isInBlackList.value) ||
(isGroup.value && !isGroupMember.value && (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
) {
contactMoreInfoConfig.setWords.data = '';
contactInfoMoreList.value.push(contactMoreInfoConfig.setWords);
}
if (!isGroup.value && !isInBlackList.value) {
contactMoreInfoConfig.setRemark.data
= (contactInfoData.value as Friend)?.remark || '';
contactMoreInfoConfig.setRemark.data = (contactInfoData.value as Friend)?.remark || '';
contactMoreInfoConfig.setRemark.editing = false;
contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark);
}
@ -336,8 +216,7 @@ const generateMoreInfo = async () => {
contactInfoMoreList.value.push(contactMoreInfoConfig.blackList);
}
} else {
contactMoreInfoConfig.displayWords.data
= (contactInfoData.value as FriendApplication)?.wording || '';
contactMoreInfoConfig.displayWords.data = (contactInfoData.value as FriendApplication)?.wording || '';
contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords);
}
};
@ -347,16 +226,9 @@ const generateButton = () => {
return;
}
if (isApplication.value) {
if (
(contactInfoData.value as FriendApplication)?.type
=== TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
contactInfoButtonList?.value?.push(
contactButtonConfig.refuseFriendApplication,
);
contactInfoButtonList?.value?.push(
contactButtonConfig.acceptFriendApplication,
);
if ((contactInfoData.value as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME) {
contactInfoButtonList?.value?.push(contactButtonConfig.refuseFriendApplication);
contactInfoButtonList?.value?.push(contactButtonConfig.acceptFriendApplication);
}
} else {
if (isGroup.value && isGroupMember.value) {
@ -368,20 +240,16 @@ const generateButton = () => {
contactInfoButtonList?.value?.push(contactButtonConfig.quitGroup);
break;
}
contactInfoButtonList?.value?.push(
contactButtonConfig.enterGroupConversation,
);
contactInfoButtonList?.value?.push(contactButtonConfig.enterGroupConversation);
} else if (!isGroup.value && isBothFriend.value) {
contactInfoButtonList?.value?.push(contactButtonConfig.deleteFriend);
contactInfoButtonList?.value?.push(
contactButtonConfig.enterC2CConversation,
);
contactInfoButtonList?.value?.push(contactButtonConfig.enterC2CConversation);
} else {
if (isGroup.value) {
contactInfoButtonList?.value?.push(
(contactInfoData.value as IGroupModel)?.type === TUIChatEngine?.TYPES?.GRP_AVCHATROOM
? contactButtonConfig.joinAVChatGroup
: contactButtonConfig.joinGroup,
: contactButtonConfig.joinGroup
);
} else {
contactInfoButtonList?.value?.push(contactButtonConfig.addFriend);
@ -395,11 +263,7 @@ function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
}
async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if (
contactInfoData.value
&& contactInfo
&& JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)
) {
if (contactInfoData.value && contactInfo && JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)) {
return;
}
resetContactInfoUIData();
@ -408,9 +272,7 @@ async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if (!contactInfoData.value || Object.keys(contactInfoData.value)?.length === 0) {
return;
}
contactInfoBasicList.value = generateContactInfoBasic(
contactInfoData.value,
);
contactInfoBasicList.value = generateContactInfoBasic(contactInfoData.value);
isBothFriend.value = await isFriend(contactInfoData.value);
generateMoreInfo();
generateButton();

View File

@ -1,17 +1,13 @@
<template>
<div :class="['tui-contact-list-card', !isPC && 'tui-contact-list-card-h5']">
<div class="tui-contact-list-card-left">
<Avatar
class="tui-contact-list-card-left-avatar"
useSkeletonAnimation
:url="generateAvatar(props.item)"
/>
<Avatar class="tui-contact-list-card-left-avatar" useSkeletonAnimation :url="generateAvatar(props.item)" />
<div
v-if="props.displayOnlineStatus && props.item"
:class="{
'online-status': true,
'online-status-online': isOnline,
'online-status-offline': !isOnline,
'online-status-offline': !isOnline
}"
/>
</div>
@ -19,28 +15,16 @@
<div class="tui-contact-list-card-main-name">
{{ generateName(props.item) }}
</div>
<div
v-if="otherContentForSow"
class="tui-contact-list-card-main-other"
>
<div v-if="otherContentForSow" class="tui-contact-list-card-main-other">
{{ otherContentForSow }}
</div>
</div>
<div class="tui-contact-list-card-right">
<div
v-if="groupTypeForShow"
class="tui-contact-list-card-right-group-type"
>
<div v-if="groupTypeForShow" class="tui-contact-list-card-right-group-type">
{{ groupTypeForShow }}
</div>
<div
v-if="showApplicationStatus"
class="tui-contact-list-card-right-application"
>
<div
v-if="showApplicationStatus.style === 'text'"
class="tui-contact-list-card-right-application-text"
>
<div v-if="showApplicationStatus" class="tui-contact-list-card-right-application">
<div v-if="showApplicationStatus.style === 'text'" class="tui-contact-list-card-right-application-text">
{{ TUITranslateService.t(`TUIContact.${showApplicationStatus.label}`) }}
</div>
<button
@ -56,12 +40,7 @@
</template>
<script setup lang="ts">
import { computed, withDefaults, inject, watch, ref, Ref } from '../../../../adapter-vue';
import TUIChatEngine, {
TUITranslateService,
IGroupModel,
FriendApplication,
Friend,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUITranslateService, IGroupModel, FriendApplication, Friend } from '@tencentcloud/chat-uikit-engine';
import { IContactInfoType, IUserStatus } from '../../../../interface';
import Avatar from '../../../common/Avatar/index.vue';
import { generateAvatar, generateName, acceptFriendApplication } from '../../utils';
@ -73,9 +52,9 @@ const props = withDefaults(
displayOnlineStatus?: boolean;
}>(),
{
item: () => ({} as IContactInfoType),
displayOnlineStatus: false,
},
item: () => ({}) as IContactInfoType,
displayOnlineStatus: false
}
);
const userOnlineStatusMap = inject<Ref<Map<string, IUserStatus>>>('userOnlineStatusMap');
const isOnline = ref<boolean>(false);
@ -85,14 +64,14 @@ const groupType = {
[TUIChatEngine.TYPES.GRP_AVCHATROOM]: 'AVChatRoom',
[TUIChatEngine.TYPES.GRP_PUBLIC]: 'Public',
[TUIChatEngine.TYPES.GRP_MEETING]: 'Meeting',
[TUIChatEngine.TYPES.GRP_COMMUNITY]: 'Community',
[TUIChatEngine.TYPES.GRP_COMMUNITY]: 'Community'
};
const otherContentForSow = computed((): string => {
let content = '';
if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
|| (props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME ||
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) {
content = (props.item as FriendApplication)?.wording || '';
} else if ((props.item as IGroupModel)?.groupID) {
@ -110,22 +89,18 @@ const groupTypeForShow = computed((): string => {
});
const showApplicationStatus = computed(() => {
if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) {
if ((props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME) {
return {
style: 'text',
label: '等待验证',
label: '等待验证'
};
} else if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
} else if ((props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME) {
return {
style: 'button',
label: '同意',
onClick: () => {
acceptFriendApplication((props.item as FriendApplication)?.userID);
},
}
};
}
return false;
@ -138,16 +113,16 @@ watch(
},
{
immediate: true,
deep: true,
},
deep: true
}
);
function getOnlineStatus(): boolean {
return !!(
props.displayOnlineStatus
&& userOnlineStatusMap?.value
&& (props.item as Friend)?.userID
&& userOnlineStatusMap.value?.[(props.item as Friend).userID]?.statusType === TUIChatEngine.TYPES.USER_STATUS_ONLINE
props.displayOnlineStatus &&
userOnlineStatusMap?.value &&
(props.item as Friend)?.userID &&
userOnlineStatusMap.value?.[(props.item as Friend).userID]?.statusType === TUIChatEngine.TYPES.USER_STATUS_ONLINE
);
}
</script>

View File

@ -1,30 +1,13 @@
<template>
<ul
v-if="!contactSearchingStatus"
:class="['tui-contact-list', !isPC && 'tui-contact-list-h5']"
>
<li
v-for="(contactListObj, key) in contactListMap"
:key="key"
class="tui-contact-list-item"
>
<header
class="tui-contact-list-item-header"
@click="toggleCurrentContactList(key)"
>
<ul v-if="!contactSearchingStatus" :class="['tui-contact-list', !isPC && 'tui-contact-list-h5']">
<li v-for="(contactListObj, key) in contactListMap" :key="key" class="tui-contact-list-item">
<header class="tui-contact-list-item-header" @click="toggleCurrentContactList(key)">
<div class="tui-contact-list-item-header-left">
<Icon
:file="currentContactListKey === key ? downSVG : rightSVG"
width="16px"
height="16px"
/>
<Icon :file="currentContactListKey === key ? downSVG : rightSVG" width="16px" height="16px" />
<div>{{ TUITranslateService.t(`TUIContact.${contactListObj.title}`) }}</div>
</div>
<div class="tui-contact-list-item-header-right">
<span
v-if="contactListObj.unreadCount"
class="tui-contact-list-item-header-right-unread"
>
<span v-if="contactListObj.unreadCount" class="tui-contact-list-item-header-right-unread">
{{ contactListObj.unreadCount }}
</span>
</div>
@ -46,19 +29,9 @@
</ul>
</li>
</ul>
<ul
v-else
class="tui-contact-list"
>
<li
v-for="(item, key) in contactSearchResult"
:key="key"
class="tui-contact-list-item"
>
<div
v-if="item.list[0]"
class="tui-contact-search-list"
>
<ul v-else class="tui-contact-list">
<li v-for="(item, key) in contactSearchResult" :key="key" class="tui-contact-list-item">
<div v-if="item.list[0]" class="tui-contact-search-list">
<div class="tui-contact-search-list-title">
{{ TUITranslateService.t(`TUIContact.${item.label}`) }}
</div>
@ -69,18 +42,12 @@
:class="['selected']"
@click="selectItem(listItem)"
>
<ContactListItem
:item="listItem"
:displayOnlineStatus="false"
/>
<ContactListItem :item="listItem" :displayOnlineStatus="false" />
</div>
</div>
</li>
<div
v-if="isContactSearchNoResult"
class="tui-contact-search-list-default"
>
{{ TUITranslateService.t("TUIContact.无搜索结果") }}
<div v-if="isContactSearchNoResult" class="tui-contact-search-list-default">
{{ TUITranslateService.t('TUIContact.无搜索结果') }}
</div>
</ul>
</template>
@ -93,21 +60,14 @@ import {
TUIFriendService,
Friend,
FriendApplication,
TUIUserService,
TUIUserService
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { ref, computed, onMounted, onUnmounted, provide } from '../../../adapter-vue';
import Icon from '../../common/Icon.vue';
import downSVG from '../../../assets/icon/down-icon.svg';
import rightSVG from '../../../assets/icon/right-icon.svg';
import {
IContactList,
IContactSearchResult,
IBlackListUserItem,
IUserStatus,
IUserStatusMap,
IContactInfoType,
} from '../../../interface';
import { IContactList, IContactSearchResult, IBlackListUserItem, IUserStatus, IUserStatusMap, IContactInfoType } from '../../../interface';
import ContactListItem from './contact-list-item/index.vue';
import { deepCopy } from '../../TUIChat/utils/utils';
import { isPC } from '../../../utils/env';
@ -119,23 +79,23 @@ const contactListMap = ref<IContactList>({
key: 'friendApplicationList',
title: '新的联系人',
list: [] as FriendApplication[],
unreadCount: 0,
unreadCount: 0
},
blackList: {
key: 'blackList',
title: '黑名单',
list: [] as IBlackListUserItem[],
list: [] as IBlackListUserItem[]
},
groupList: {
key: 'groupList',
title: '我的群聊',
list: [] as IGroupModel[],
list: [] as IGroupModel[]
},
friendList: {
key: 'friendList',
title: '我的好友',
list: [] as Friend[],
},
list: [] as Friend[]
}
});
const contactSearchingStatus = ref<boolean>(false);
const contactSearchResult = ref<IContactSearchResult>();
@ -143,67 +103,64 @@ const displayOnlineStatus = ref<boolean>(false);
const userOnlineStatusMap = ref<IUserStatusMap>();
const isContactSearchNoResult = computed((): boolean => {
return (
!contactSearchResult?.value?.user?.list[0]
&& !contactSearchResult?.value?.group?.list[0]
);
return !contactSearchResult?.value?.user?.list[0] && !contactSearchResult?.value?.group?.list[0];
});
onMounted(() => {
TUIStore.watch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated
});
TUIStore.watch(StoreName.GRP, {
groupList: onGroupListUpdated,
groupList: onGroupListUpdated
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
userStatusList: onUserStatusListUpdated
});
TUIStore.watch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
});
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactInfo: onCurrentContactInfoUpdated
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated
});
TUIStore.unwatch(StoreName.GRP, {
groupList: onGroupListUpdated,
groupList: onGroupListUpdated
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
userStatusList: onUserStatusListUpdated
});
TUIStore.unwatch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
});
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactInfo: onCurrentContactInfoUpdated
});
});
@ -230,11 +187,11 @@ function selectItem(item: any) {
let targetListItem;
if ((currentContactInfo.value as Friend)?.userID) {
targetListItem = contactListMap.value?.friendList?.list?.find(
(item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID,
(item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID
);
} else if ((currentContactInfo.value as IGroupModel)?.groupID) {
targetListItem = contactListMap.value?.groupList?.list?.find(
(item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID,
(item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID
);
}
if (targetListItem) {
@ -283,15 +240,15 @@ function onCustomerServiceCommercialPluginUpdated(isEnabled: boolean) {
...item,
renderKey: generateRenderKey('customerList', item, index),
infoKeyList: [],
btnKeyList: ['enterC2CConversation'],
btnKeyList: ['enterC2CConversation']
};
}),
key: 'customerList',
key: 'customerList'
};
contactListMap.value = { ...contactListMap.value, customerList };
}
})
.catch(() => { });
.catch(() => {});
}
}
@ -317,28 +274,27 @@ function onFriendApplicationListUpdated(friendApplicationList: FriendApplication
function updateContactListMap(key: keyof IContactList, list: IContactInfoType[]) {
contactListMap.value[key].list = list;
contactListMap.value[key].list.map((item: IContactInfoType, index: number) => item.renderKey = generateRenderKey(key, item, index));
contactListMap.value[key].list.map((item: IContactInfoType, index: number) => (item.renderKey = generateRenderKey(key, item, index)));
updateCurrentContactInfoFromList(contactListMap.value[key].list, key);
}
function updateCurrentContactInfoFromList(list: IContactInfoType[], type: keyof IContactList) {
if (
!(currentContactInfo.value as Friend)?.userID
&& !(currentContactInfo.value as IGroupModel)?.groupID
) {
if (!(currentContactInfo.value as Friend)?.userID && !(currentContactInfo.value as IGroupModel)?.groupID) {
return;
}
if (type === currentContactListKey.value || contactSearchingStatus.value) {
currentContactInfo.value = list?.find(
currentContactInfo.value =
list?.find(
(item: any) =>
(item?.groupID && item?.groupID === (currentContactInfo.value as IGroupModel)?.groupID) || (item?.userID && item?.userID === (currentContactInfo.value as Friend)?.userID),
) || {} as IContactInfoType;
(item?.groupID && item?.groupID === (currentContactInfo.value as IGroupModel)?.groupID) ||
(item?.userID && item?.userID === (currentContactInfo.value as Friend)?.userID)
) || ({} as IContactInfoType);
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
}
}
function generateRenderKey(contactListMapKey: keyof IContactList, contactInfo: IContactInfoType, index: number) {
return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || ('index' + index)}`;
return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || 'index' + index}`;
}
function onCurrentContactSearchResultUpdated(searchResult: IContactSearchResult) {

View File

@ -2,43 +2,21 @@
<div :class="['tui-contact-search', !isPC && 'tui-contact-search-h5']">
<div
v-if="!isSearching || !isPC"
:class="[
'tui-contact-search-header',
!isPC && 'tui-contact-search-h5-header',
isSearching && 'tui-contact-searching-h5-header',
]"
:class="['tui-contact-search-header', !isPC && 'tui-contact-search-h5-header', isSearching && 'tui-contact-searching-h5-header']"
@click="changeContactSearchingStatus(true)"
>
<div
:class="[
'tui-contact-search-header-icon',
!isPC && 'tui-contact-search-h5-header-icon',
]"
:class="['tui-contact-search-header-icon', !isPC && 'tui-contact-search-h5-header-icon']"
@click.stop="changeContactSearchingStatus(!isSearching)"
>
<Icon
:file="isSearching ? backSVG : addSVG"
:width="isSearching ? '20px' : '14px'"
:height="isSearching ? '20px' : '14px'"
/>
<Icon :file="isSearching ? backSVG : addSVG" :width="isSearching ? '20px' : '14px'" :height="isSearching ? '20px' : '14px'" />
</div>
<div
:class="[
'tui-contact-search-header-title',
!isPC && 'tui-contact-search-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
<div :class="['tui-contact-search-header-title', !isPC && 'tui-contact-search-h5-header-title']">
{{ TUITranslateService.t('TUIContact.添加好友/群聊') }}
</div>
</div>
<div
v-if="isSearching"
:class="[
'tui-contact-search-main',
!isPC && 'tui-contact-search-h5-main',
]"
>
<div v-if="isSearching" :class="['tui-contact-search-main', !isPC && 'tui-contact-search-h5-main']">
<input
v-model="searchValue"
class="tui-contact-search-main-input"
@ -48,23 +26,16 @@
@keyup.enter="search"
@blur="search"
@confirm="search"
>
<div
class="tui-contact-search-main-cancel"
@click="isSearching = false"
>
{{ TUITranslateService.t("取消") }}
/>
<div class="tui-contact-search-main-cancel" @click="isSearching = false">
{{ TUITranslateService.t('取消') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from '../../../adapter-vue';
import {
TUITranslateService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { TUIGlobal } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue';
@ -80,12 +51,12 @@ const searchValue = ref<string>('');
const searchResult = ref<IContactSearchResult>({
user: {
label: '联系人',
list: [],
list: []
},
group: {
label: '群聊',
list: [],
},
list: []
}
});
const changeContactSearchingStatus = debounce(function (status: boolean) {
@ -100,8 +71,8 @@ const search = async () => {
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER,
params: {
userID: searchValue.value,
},
userID: searchValue.value
}
})
.then((res: any) => {
searchResult.value.user.list = res.data;
@ -114,8 +85,8 @@ const search = async () => {
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP,
params: {
groupID: searchValue.value,
},
groupID: searchValue.value
}
})
.then((res: any) => {
searchResult.value.group.list = [res.data.group];
@ -128,25 +99,17 @@ const search = async () => {
watch(
() => searchResult.value,
() => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchResult',
searchResult.value,
);
TUIStore.update(StoreName.CUSTOM, 'currentContactSearchResult', searchResult.value);
},
{
deep: true,
immediate: true,
},
immediate: true
}
);
watch(
() => isSearching.value,
() => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
isSearching.value,
);
TUIStore.update(StoreName.CUSTOM, 'currentContactSearchingStatus', isSearching.value);
if (isSearching.value) {
searchValue.value = '';
searchResult.value.user.list = [];
@ -155,8 +118,8 @@ watch(
},
{
deep: true,
immediate: true,
},
immediate: true
}
);
TUIGlobal.updateContactSearch = search;

View File

@ -1,17 +1,11 @@
<template>
<SelectFriend v-if="isShowSelectFriend" />
<div
v-else-if="isShowContactList"
:class="['tui-contact', !isPC && 'tui-contact-h5']"
>
<div v-else-if="isShowContactList" :class="['tui-contact', !isPC && 'tui-contact-h5']">
<div :class="['tui-contact-left', !isPC && 'tui-contact-h5-left']">
<ContactSearch />
<ContactList :class="['tui-contact-left-list', !isPC && 'tui-contact-h5-left-list']" />
</div>
<div
v-if="isShowContactInfo"
:class="['tui-contact-right', !isPC && 'tui-contact-h5-right']"
>
<div v-if="isShowContactInfo" :class="['tui-contact-right', !isPC && 'tui-contact-h5-right']">
<ContactInfo @switchConversation="switchConversation" />
</div>
</div>
@ -34,8 +28,8 @@ const props = defineProps({
displayType: {
type: String,
default: 'contactList', // "contactList" / "selectFriend"
require: false,
},
require: false
}
});
const displayTypeRef = ref<string>(props.displayType || 'contactList');
@ -64,26 +58,27 @@ TUIStore.watch(StoreName.CUSTOM, {
isShowSelectFriend.value = false;
if (isUniFrameWork) {
displayTypeRef.value = props.displayType;
TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ });
TUIGlobal?.showTabBar()?.catch(() => {
/* ignore */
});
}
}
},
currentContactInfo: (contactInfo: any) => {
isShowContactInfo.value = isPC || (contactInfo && typeof contactInfo === 'object' && Object.keys(contactInfo)?.length > 0);
},
}
});
const switchConversation = (data: any) => {
isUniFrameWork
&& TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index',
isUniFrameWork &&
TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index'
});
emits('switchConversation', data);
};
</script>
<style lang="scss" scoped>
@import "../../assets/styles/common";
@import '../../assets/styles/common';
.tui-contact {
width: 100%;

View File

@ -9,12 +9,7 @@
/>
</template>
<script lang="ts" setup>
import {
TUIFriendService,
TUIStore,
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIFriendService, TUIStore, StoreName, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { ref, watchEffect } from '../../../adapter-vue';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import TUICore from '@tencentcloud/tui-core';
@ -30,7 +25,7 @@ const TUISearchServer = ref<any>(null);
const selectOptions = ref({
isRadio: false,
isNeedSearch: false,
title: '',
title: ''
});
const generateSearchServer = (isNeedSearch: any) => {
@ -50,10 +45,12 @@ watchEffect(() => {
if (params.isNeedSearch) {
generateSearchServer(params.isNeedSearch);
}
TUIFriendService.getFriendList().then((res: any) => {
TUIFriendService.getFriendList()
.then((res: any) => {
friendList.value = res.data.map((item: any) => item.profile);
userList.value = friendList.value;
}).catch((err: any) => {
})
.catch((err: any) => {
console.warn('getFriendList error:', err);
});
});
@ -67,14 +64,14 @@ const handleSelectedResult = (memberList: Array<any>) => {
const searchFail = () => {
Toast({
message: TUITranslateService.t('TUIGroup.该用户不存在'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
userList.value = [...friendList.value];
};
const handleSearch = async (val: string) => {
if (!val) {
return userList.value = friendList.value;
return (userList.value = friendList.value);
}
try {

View File

@ -35,8 +35,9 @@ export default class TUIContactServer {
this.onCallCallbackMap.set(method, callback);
if (method === TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND) {
TUIStore.update(StoreName.CUSTOM, 'isShowSelectFriendComponent', true);
isUniFrameWork && TUIGlobal?.reLaunch({
url: '/TUIKit/components/TUIContact/index',
isUniFrameWork &&
TUIGlobal?.reLaunch({
url: '/TUIKit/components/TUIContact/index'
});
}
}

View File

@ -5,69 +5,49 @@ import TUIChatEngine, {
TUIUserService,
TUITranslateService,
AddFriendParams,
JoinGroupParams,
JoinGroupParams
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
export const generateAvatar = (item: any): string => {
return (
item?.avatar
|| item?.profile?.avatar
|| (item?.groupID && 'https://web.sdk.qcloud.com/im/assets/images/Public.svg')
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
item?.avatar ||
item?.profile?.avatar ||
(item?.groupID && 'https://web.sdk.qcloud.com/im/assets/images/Public.svg') ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
);
};
export const generateName = (item: any): string => {
return (
item?.remark
|| item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
return item?.remark || item?.name || item?.profile?.nick || item?.nick || item?.groupID || item?.userID || '';
};
export const generateContactInfoName = (item: any): string => {
return (
item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
return item?.name || item?.profile?.nick || item?.nick || item?.groupID || item?.userID || '';
};
// Parse the basic information display content of the contactInfo module
// Group information display: group ID group type
// User information display: user ID personal signature
export const generateContactInfoBasic = (
contactInfo: any,
): any[] => {
export const generateContactInfoBasic = (contactInfo: any): any[] => {
const res = [
{
label: contactInfo?.groupID ? '群ID' : 'ID',
data: contactInfo?.groupID || contactInfo?.userID || '',
},
data: contactInfo?.groupID || contactInfo?.userID || ''
}
];
if (!isApplicationType(contactInfo)) {
res.push({
label: contactInfo?.groupID ? '群类型' : '个性签名',
data: contactInfo?.type || contactInfo?.profile?.selfSignature || '',
data: contactInfo?.type || contactInfo?.profile?.selfSignature || ''
});
}
return res;
};
export const isApplicationType = (info: any) => {
return (
info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
|| info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
);
return info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME || info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME;
};
export const isFriend = (info: any): Promise<boolean> => {
@ -82,7 +62,7 @@ export const isFriend = (info: any): Promise<boolean> => {
}
TUIFriendService.checkFriend({
userIDList: [info?.userID],
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH,
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH
})
.then((res: any) => {
switch (res?.data?.successUserIDList[0]?.relation) {
@ -116,29 +96,28 @@ export const isFriend = (info: any): Promise<boolean> => {
// Change friends remark
export const updateFriendRemark = (userID: string, remark: string) => {
// eslint-disable-next-line no-control-regex
if (remark?.replace(/[^\u0000-\u00ff]/g, 'aa')?.length > 96) {
Toast({
message: TUITranslateService.t('TUIContact.修改备注失败: 备注长度不得超过 96 字节'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
return;
}
TUIFriendService.updateFriend({
userID,
remark,
remark
})
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.修改备注成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
})
.catch((error: any) => {
console.warn('update friend remark failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.修改备注失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -147,19 +126,19 @@ export const updateFriendRemark = (userID: string, remark: string) => {
export const deleteFriend = (userID: string) => {
TUIFriendService.deleteFriend({
userIDList: [userID],
type: TUIChatEngine.TYPES.SNS_DELETE_TYPE_BOTH,
type: TUIChatEngine.TYPES.SNS_DELETE_TYPE_BOTH
})
.then((res: any) => {
const { successUserIDList } = res.data;
if (successUserIDList[0].userID === userID) {
Toast({
message: TUITranslateService.t('TUIContact.删除好友成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
} else {
Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
})
@ -167,7 +146,7 @@ export const deleteFriend = (userID: string) => {
console.warn('delete friend failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -178,51 +157,47 @@ export const addFriend = (params: AddFriendParams) => {
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.申请已发送'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
})
.catch((error: any) => {
console.warn('delete friend failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.申请发送失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
// Enter conversation
export const enterConversation = (item: any) => {
const conversationID = item?.groupID
? `GROUP${item?.groupID}`
: `C2C${item?.userID}`;
TUIConversationService.switchConversation(conversationID).catch(
(error: any) => {
const conversationID = item?.groupID ? `GROUP${item?.groupID}` : `C2C${item?.userID}`;
TUIConversationService.switchConversation(conversationID).catch((error: any) => {
console.warn('switch conversation failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.进入会话失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
},
);
};
// Accept friend application
export const acceptFriendApplication = (userID: string) => {
TUIFriendService.acceptFriendApplication({
userID,
type: TUIChatEngine.TYPES.SNS_APPLICATION_AGREE_AND_ADD,
type: TUIChatEngine.TYPES.SNS_APPLICATION_AGREE_AND_ADD
})
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.添加好友成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
})
.catch((error: any) => {
console.warn('accept friend application failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.同意好友申请失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -233,14 +208,14 @@ export const refuseFriendApplication = (userID: string) => {
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.拒绝成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
})
.catch((error: any) => {
console.warn('accept friend application failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.拒绝好友申请失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -251,7 +226,7 @@ export const dismissGroup = (groupID: string) => {
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.解散群聊成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch();
})
@ -259,7 +234,7 @@ export const dismissGroup = (groupID: string) => {
console.warn('dismiss group failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.解散群聊失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -270,14 +245,14 @@ export const quitGroup = (groupID: string) => {
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.退出群组成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
})
.catch((error: any) => {
console.warn('quit group failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.退出群组失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -286,26 +261,26 @@ export const quitGroup = (groupID: string) => {
export const joinGroup = (groupID: string, applyMessage?: string) => {
TUIGroupService.joinGroup({
groupID,
applyMessage,
applyMessage
} as JoinGroupParams)
.then((imResponse: { data: { status?: string } }) => {
switch (imResponse?.data?.status) {
case TUIChatEngine.TYPES.JOIN_STATUS_WAIT_APPROVAL: // Wait for administrator approval
Toast({
message: TUITranslateService.t('TUIContact.等待管理员同意'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
break;
case TUIChatEngine.TYPES.JOIN_STATUS_SUCCESS: // Join group successfully
Toast({
message: TUITranslateService.t('TUIContact.加群成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
break;
case TUIChatEngine.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // Already in the group
Toast({
message: TUITranslateService.t('TUIContact.您已是群成员'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
break;
default:
@ -316,7 +291,7 @@ export const joinGroup = (groupID: string, applyMessage?: string) => {
console.warn('join group failed:', error);
Toast({
message: '申请入群失败',
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -324,7 +299,7 @@ export const joinGroup = (groupID: string, applyMessage?: string) => {
// Add to blacklist
export const addToBlacklist = (userID: string, successCallBack?: () => void) => {
TUIUserService.addToBlacklist({
userIDList: [userID],
userIDList: [userID]
})
.then(() => {
successCallBack && successCallBack();
@ -333,18 +308,15 @@ export const addToBlacklist = (userID: string, successCallBack?: () => void) =>
console.warn('add to blacklist failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.加入黑名单失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
// Remove from Blacklist
export const removeFromBlacklist = (
userID: string,
successCallBack?: () => void,
) => {
export const removeFromBlacklist = (userID: string, successCallBack?: () => void) => {
TUIUserService.removeFromBlacklist({
userIDList: [userID],
userIDList: [userID]
})
.then(() => {
successCallBack && successCallBack();
@ -353,7 +325,7 @@ export const removeFromBlacklist = (
console.warn('remove from blacklist failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.移除黑名单失败'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};

View File

@ -1,54 +1,44 @@
<template>
<Overlay
:maskColor="'transparent'"
@onOverlayClick="() => emits('closeConversationActionMenu')"
>
<Overlay :maskColor="'transparent'" @onOverlayClick="() => emits('closeConversationActionMenu')">
<div
id="conversation-actions-menu"
ref="actionsMenuDomRef"
:class="[
isPC && 'actions-menu-pc',
'actions-menu',
!isHiddenActionsMenu && 'cancel-hidden',
]"
:class="[isPC && 'actions-menu-pc', 'actions-menu', !isHiddenActionsMenu && 'cancel-hidden']"
:style="{
top: `${_actionsMenuPosition.top}px`,
left: `${_actionsMenuPosition.left}px`,
left: `${_actionsMenuPosition.left}px`
}"
>
<div
:class="['actions-menu-item']"
@click.stop="deleteConversation()"
>
{{ TUITranslateService.t("TUIConversation.删除会话") }}
<div :class="['actions-menu-item']" @click.stop="deleteConversation()">
{{ TUITranslateService.t('TUIConversation.删除会话') }}
</div>
<div
v-if="!(props.selectedConversation && props.selectedConversation.isPinned)"
:class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.ISPINNED })"
>
{{ TUITranslateService.t("TUIConversation.置顶会话") }}
{{ TUITranslateService.t('TUIConversation.置顶会话') }}
</div>
<div
v-if="props.selectedConversation && props.selectedConversation.isPinned"
:class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.DISPINNED })"
>
{{ TUITranslateService.t("TUIConversation.取消置顶") }}
{{ TUITranslateService.t('TUIConversation.取消置顶') }}
</div>
<div
v-if="!(props.selectedConversation && props.selectedConversation.isMuted)"
:class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.MUTE })"
>
{{ TUITranslateService.t("TUIConversation.消息免打扰") }}
{{ TUITranslateService.t('TUIConversation.消息免打扰') }}
</div>
<div
v-if="props.selectedConversation && props.selectedConversation.isMuted"
:class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.NOTMUTE })"
>
{{ TUITranslateService.t("TUIConversation.取消免打扰") }}
{{ TUITranslateService.t('TUIConversation.取消免打扰') }}
</div>
</div>
<Dialog
@ -66,18 +56,8 @@
</template>
<script setup lang="ts">
import {
ref,
nextTick,
onMounted,
computed,
getCurrentInstance,
} from '../../../adapter-vue';
import TUIChatEngine, {
IConversationModel,
TUIStore,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { ref, nextTick, onMounted, computed, getCurrentInstance } from '../../../adapter-vue';
import TUIChatEngine, { IConversationModel, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { CONV_OPERATION } from '../../../constant';
import { isPC, isUniFrameWork } from '../../../utils/env';
@ -101,9 +81,7 @@ const thisInstance = getCurrentInstance()?.proxy || getCurrentInstance();
const actionsMenuDomRef = ref<HTMLElement | null>();
const isHiddenActionsMenu = ref(true);
const isShowDeleteConversationDialog = ref<boolean>(false);
const currentConversation = TUIStore.getConversationModel(
props.selectedConversation?.conversationID || '',
);
const currentConversation = TUIStore.getConversationModel(props.selectedConversation?.conversationID || '');
const _actionsMenuPosition = ref<{
top: number;
left?: number;
@ -117,7 +95,9 @@ onMounted(() => {
const deleteConversationDialogTitle = computed(() => {
return props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_C2C
? 'TUIConversation.删除后,将清空该聊天的消息记录'
: props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_GROUP ? 'TUIConversation.删除后,将清空该群聊的消息记录' : '';
: props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_GROUP
? 'TUIConversation.删除后,将清空该群聊的消息记录'
: '';
});
function checkExceedBounds() {
// When the component is initially rendered, it executes and self-checks whether the boundary exceeds the screen, and handles it in nextTick.
@ -133,10 +113,7 @@ function checkExceedBounds() {
if (data.bottom > TUIGlobal?.getWindowInfo?.().windowHeight) {
_actionsMenuPosition.value = {
...props.actionsMenuPosition,
top:
props.actionsMenuPosition.top
- (props.actionsMenuPosition.conversationHeight || 0)
- data.height,
top: props.actionsMenuPosition.top - (props.actionsMenuPosition.conversationHeight || 0) - data.height
};
}
// check if actionsMenu is exceed right of the screen
@ -155,10 +132,7 @@ function checkExceedBounds() {
_actionsMenuPosition.value.left = props.actionsMenuPosition.left;
}
if (rect && rect.bottom > window.innerHeight) {
_actionsMenuPosition.value.top
= props.actionsMenuPosition.top
- (props.actionsMenuPosition.conversationHeight || 0)
- rect.height;
_actionsMenuPosition.value.top = props.actionsMenuPosition.top - (props.actionsMenuPosition.conversationHeight || 0) - rect.height;
}
isHiddenActionsMenu.value = false;
}

View File

@ -1,54 +1,20 @@
<template>
<div
:ref="convHeaderRef"
class="tui-conversation-header"
>
<ul
v-if="menuList.length > 0"
class="list"
>
<li
v-for="(item, index) in menuList"
:key="index"
class="list-item"
>
<main
class="tui-conversation-header-item"
@click.stop="handleMenu(item)"
>
<Icon
v-if="item.icon && !item.data.children"
class="tui-conversation-header-item-icon"
:file="item.icon"
/>
<i
v-else
class="plus"
/>
<div :ref="convHeaderRef" class="tui-conversation-header">
<ul v-if="menuList.length > 0" class="list">
<li v-for="(item, index) in menuList" :key="index" class="list-item">
<main class="tui-conversation-header-item" @click.stop="handleMenu(item)">
<Icon v-if="item.icon && !item.data.children" class="tui-conversation-header-item-icon" :file="item.icon" />
<i v-else class="plus" />
<h1 class="tui-conversation-header-item-title">
{{ item.text }}
</h1>
</main>
</li>
</ul>
<ul
v-if="showChildren.length > 0"
class="tui-conversation-header-children list"
>
<li
v-for="(childrenItem, childrenIndex) in showChildren"
:key="childrenIndex"
class="list-item"
>
<main
class="tui-conversation-header-item"
@click="handleMenu(childrenItem)"
>
<Icon
v-if="childrenItem.icon"
class="tui-conversation-header-item-icon"
:file="childrenItem.icon"
/>
<ul v-if="showChildren.length > 0" class="tui-conversation-header-children list">
<li v-for="(childrenItem, childrenIndex) in showChildren" :key="childrenIndex" class="list-item">
<main class="tui-conversation-header-item" @click="handleMenu(childrenItem)">
<Icon v-if="childrenItem.icon" class="tui-conversation-header-item-icon" :file="childrenItem.icon" />
<h1 class="tui-conversation-header-item-title">
{{ childrenItem.text }}
</h1>
@ -74,7 +40,10 @@ onMounted(() => {
});
const handleMenu = (item: IMenuItem) => {
const { data: { children }, listener = { onClicked: () => {} } } = item;
const {
data: { children },
listener = { onClicked: () => {} }
} = item;
if (children) {
showChildren.value = showChildren.value.length > 0 ? [] : children;
} else {
@ -88,9 +57,8 @@ const closeChildren = () => {
};
defineExpose({
closeChildren,
closeChildren
});
</script>
<style lang="scss" scoped src="../style/index.scss"></style>

View File

@ -30,13 +30,15 @@ export default class ConversationHeaderServer {
public getMenu(): any[] {
const list = this.generateMenuList();
if (!isPC && list.length > 0) {
return [{
return [
{
text: TUITranslateService.t('TUIConversation.发起会话'),
data: {
name: 'all',
children: list,
},
}];
children: list
}
}
];
}
return list;
}
@ -47,22 +49,22 @@ export default class ConversationHeaderServer {
icon: C2C,
text: TUITranslateService.t('TUIConversation.发起单聊'),
data: {
name: CONV_CREATE_TYPE.TYPEC2C,
name: CONV_CREATE_TYPE.TYPEC2C
},
listener: {
onClicked: this.createConversation.bind(this),
},
onClicked: this.createConversation.bind(this)
}
},
{
icon: createGroup,
text: TUITranslateService.t('TUIConversation.发起群聊'),
data: {
name: CONV_CREATE_TYPE.TYPEGROUP,
name: CONV_CREATE_TYPE.TYPEGROUP
},
listener: {
onClicked: this.createConversation.bind(this),
},
},
onClicked: this.createConversation.bind(this)
}
}
];
return list;
}
@ -72,7 +74,7 @@ export default class ConversationHeaderServer {
TUICore.callService({
serviceName: TUIConstants.TUIConversation.SERVICE.NAME,
method: TUIConstants.TUIConversation.SERVICE.METHOD.CREATE_CONVERSATION,
params: item,
params: item
});
}
}

View File

@ -1,8 +1,5 @@
<template>
<div
ref="conversationListInnerDomRef"
class="tui-conversation-list"
>
<div ref="conversationListInnerDomRef" class="tui-conversation-list">
<ActionsMenu
v-if="isShowOverlay"
:selectedConversation="currentConversation"
@ -14,55 +11,36 @@
v-for="(conversation, index) in conversationList"
:id="`convlistitem-${index}`"
:key="index"
:class="[
'tui-conversation-content',
isMobile && 'tui-conversation-content-h5 disable-select',
]"
:class="['tui-conversation-content', isMobile && 'tui-conversation-content-h5 disable-select']"
>
<div
:class="[
isPC && 'isPC',
'tui-conversation-item',
currentConversationID === conversation.conversationID &&
'tui-conversation-item-selected',
conversation.isPinned && 'tui-conversation-item-pinned',
currentConversationID === conversation.conversationID && 'tui-conversation-item-selected',
conversation.isPinned && 'tui-conversation-item-pinned'
]"
@click="enterConversationChat(conversation.conversationID)"
@longpress="showConversationActionMenu($event, conversation, index)"
@contextmenu="showConversationActionMenu($event, conversation, index, true)"
>
<aside class="left">
<Avatar
useSkeletonAnimation
:url="conversation.getAvatar()"
size="30px"
/>
<Avatar useSkeletonAnimation :url="conversation.getAvatar()" size="30px" />
<div
v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)"
:class="[
'online-status',
Object.keys(userOnlineStatusMap).length > 0 &&
Object.keys(userOnlineStatusMap).includes(
conversation.userProfile.userID
) &&
userOnlineStatusMap[conversation.userProfile.userID]
.statusType === 1
Object.keys(userOnlineStatusMap).includes(conversation.userProfile.userID) &&
userOnlineStatusMap[conversation.userProfile.userID].statusType === 1
? 'online-status-online'
: 'online-status-offline',
: 'online-status-offline'
]"
/>
<span
v-if="conversation.unreadCount > 0 && !conversation.isMuted"
class="num"
>
{{
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
}}
<span v-if="conversation.unreadCount > 0 && !conversation.isMuted" class="num">
{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
</span>
<span
v-if="conversation.unreadCount > 0 && conversation.isMuted"
class="num-notify"
/>
<span v-if="conversation.unreadCount > 0 && conversation.isMuted" class="num-notify" />
</aside>
<div class="content">
<div class="content-header">
@ -70,29 +48,22 @@
<p class="name">{{ conversation.getShowName() }}</p>
</label>
<div class="middle-box">
<span v-if="conversation.draftText && conversation.conversationID !== currentConversationID" class="middle-box-draft">{{
TUITranslateService.t('TUIChat.[草稿]')
}}</span>
<span
v-if="conversation.draftText && conversation.conversationID !== currentConversationID"
class="middle-box-draft"
>{{ TUITranslateService.t('TUIChat.[草稿]') }}</span>
<span
v-else-if="
conversation.type === 'GROUP' &&
conversation.groupAtInfoList &&
conversation.groupAtInfoList.length > 0
"
v-else-if="conversation.type === 'GROUP' && conversation.groupAtInfoList && conversation.groupAtInfoList.length > 0"
class="middle-box-at"
>{{ conversation.getGroupAtInfo() }}</span>
>{{ conversation.getGroupAtInfo() }}</span
>
<div class="middle-box-content">
{{ conversation.getLastMessage("text") }}
{{ conversation.getLastMessage('text') }}
</div>
</div>
</div>
<div class="content-footer">
<span class="time">{{ conversation.getLastMessage("time") }}</span>
<Icon
v-if="conversation.isMuted"
:file="muteIcon"
/>
<span class="time">{{ conversation.getLastMessage('time') }}</span>
<Icon v-if="conversation.isMuted" :file="muteIcon" />
</div>
</div>
</div>
@ -111,13 +82,7 @@ interface IUserStatusMap {
}
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
import TUIChatEngine, {
TUIStore,
StoreName,
TUIConversationService,
TUITranslateService,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUIStore, StoreName, TUIConversationService, TUITranslateService, IConversationModel } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal, isIOS, addLongPressListener } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue';
import Avatar from '../../common/Avatar/index.vue';
@ -140,7 +105,7 @@ const actionsMenuPosition = ref<{
}>({
top: 0,
left: undefined,
conversationHeight: undefined,
conversationHeight: undefined
});
const displayOnlineStatus = ref(false);
const userOnlineStatusMap = ref<IUserStatusMap>();
@ -151,12 +116,12 @@ onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
TUIStore.watch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
userStatusList: onUserStatusListUpdated
});
if (!isUniFrameWork && isIOS && !isPC) {
@ -168,28 +133,20 @@ onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated,
currentConversation: onCurrentConversationUpdated
});
TUIStore.unwatch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
userStatusList: onUserStatusListUpdated
});
});
const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => {
return (
displayOnlineStatus.value
&& conversation.type === TUIChatEngine.TYPES.CONV_C2C
);
return displayOnlineStatus.value && conversation.type === TUIChatEngine.TYPES.CONV_C2C;
};
const showConversationActionMenu = (
event: Event,
conversation: IConversationModel,
index: number,
isContextMenuEvent?: boolean,
) => {
const showConversationActionMenu = (event: Event, conversation: IConversationModel, index: number, isContextMenuEvent?: boolean) => {
if (isContextMenuEvent) {
event.preventDefault();
if (isUniFrameWork) {
@ -203,10 +160,7 @@ const showConversationActionMenu = (
const closeConversationActionMenu = () => {
// Prevent continuous triggering of overlay tap events
if (
lastestOpenActionsMenuTime
&& Date.now() - lastestOpenActionsMenuTime > 300
) {
if (lastestOpenActionsMenuTime && Date.now() - lastestOpenActionsMenuTime > 300) {
currentConversation.value = undefined;
isShowOverlay.value = false;
}
@ -218,25 +172,28 @@ const getActionsMenuPosition = (event: Event, index: number) => {
emits('getPassingRef', conversationListDomRef);
}
const query = TUIGlobal?.createSelectorQuery().in(conversationListDomRef.value);
query.select(`#convlistitem-${index}`).boundingClientRect((data) => {
query
.select(`#convlistitem-${index}`)
.boundingClientRect((data) => {
if (data) {
actionsMenuPosition.value = {
// The uni-page-head of uni-h5 is not considered a member of the viewport, so the height of the head is manually increased.
top: data.bottom + (isH5 ? 44 : 0),
// @ts-expect-error in uniapp event has touches property
left: event.touches[0].pageX,
conversationHeight: data.height,
conversationHeight: data.height
};
isShowOverlay.value = true;
}
}).exec();
})
.exec();
} else {
const rect = ((event.currentTarget || event.target) as HTMLElement)?.getBoundingClientRect() || {};
if (rect) {
actionsMenuPosition.value = {
top: rect.bottom,
left: isPC ? (event as MouseEvent).clientX : undefined,
conversationHeight: rect.height,
conversationHeight: rect.height
};
}
isShowOverlay.value = true;
@ -260,9 +217,9 @@ function addLongPressHandler() {
},
options: {
eventDelegation: {
subSelector: '.tui-conversation-content',
},
},
subSelector: '.tui-conversation-content'
}
}
});
}
@ -284,13 +241,10 @@ function onDisplayOnlineStatusUpdated(status: boolean) {
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list.size !== 0) {
userOnlineStatusMap.value = [...list.entries()].reduce(
(obj, [key, value]) => {
userOnlineStatusMap.value = [...list.entries()].reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
},
{} as IUserStatusMap,
);
}, {} as IUserStatusMap);
}
}
// Expose to the parent component and close actionsMenu when a sliding event is detected
@ -300,11 +254,11 @@ defineExpose({ closeChildren: closeConversationActionMenu });
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped>
.disable-select {
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

View File

@ -1,32 +1,21 @@
<template>
<div
v-if="isNotNetwork"
class="network"
>
<div v-if="isNotNetwork" class="network">
<i class="icon icon-error">!</i>
<p class="network-content">
{{
TUITranslateService.t("TUIConversation.网络异常,请您检查网络设置")
}}
{{ TUITranslateService.t('TUIConversation.网络异常,请您检查网络设置') }}
</p>
</div>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import {
ref,
} from '../../../adapter-vue';
import TUIChatEngine, { TUIStore, StoreName, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../adapter-vue';
const isNotNetwork = ref(false);
TUIStore.watch(StoreName.USER, {
netStateChange: (value: string) => {
isNotNetwork.value = (value === TUIChatEngine.TYPES.NET_STATE_DISCONNECTED);
},
isNotNetwork.value = value === TUIChatEngine.TYPES.NET_STATE_DISCONNECTED;
}
});
</script>

View File

@ -1,5 +1,5 @@
import TUIConversation from "./index.vue";
import TUIConversationServer from "./server";
import TUIConversation from './index.vue';
import TUIConversationServer from './server';
new TUIConversationServer();
export default TUIConversation;

View File

@ -1,12 +1,6 @@
<template>
<div
class="tui-conversation"
@click="handleClickConv"
>
<ConversationHeader
v-if="isShowConversationHeader"
ref="headerRef"
/>
<div class="tui-conversation" @click="handleClickConv">
<ConversationHeader v-if="isShowConversationHeader" ref="headerRef" />
<ConversationNetwork />
<ConversationList @handleSwitchConversation="handleSwitchConversation" />
</div>
@ -27,13 +21,13 @@ const isShowConversationHeader = ref<boolean>(true);
TUIStore.watch(StoreName.CONV, {
totalUnreadCount: (count: number) => {
totalUnreadCount.value = count;
},
}
});
TUIStore.watch(StoreName.CUSTOM, {
isShowConversationHeader: (showStatus: boolean) => {
isShowConversationHeader.value = showStatus !== false;
},
}
});
const handleSwitchConversation = (conversationID: string) => {

View File

@ -1,10 +1,5 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import {
TUITranslateService,
TUIConversationService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, TUIConversationService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { CONV_CREATE_TYPE } from '../../constant';
import { isUniFrameWork } from '../../utils/env';
@ -61,23 +56,23 @@ export default class TUIConversationServer {
icon: createC2CIcon,
text: TUITranslateService.t('TUIConversation.发起单聊'),
data: {
name: CONV_CREATE_TYPE.TYPEC2C,
name: CONV_CREATE_TYPE.TYPEC2C
},
listener: {
onClicked: this.createConversation.bind(this),
},
onClicked: this.createConversation.bind(this)
}
},
{
weight: 100,
icon: createGroupIcon,
text: TUITranslateService.t('TUIConversation.发起群聊'),
data: {
name: CONV_CREATE_TYPE.TYPEGROUP,
name: CONV_CREATE_TYPE.TYPEGROUP
},
listener: {
onClicked: this.createConversation.bind(this),
},
},
onClicked: this.createConversation.bind(this)
}
}
];
return list;
}
@ -91,7 +86,7 @@ export default class TUIConversationServer {
params: {
title: item.text,
isRadio: item.data.name !== CONV_CREATE_TYPE.TYPEGROUP,
isNeedSearch: !TUIStore.getData(StoreName.APP, 'isOfficial'),
isNeedSearch: !TUIStore.getData(StoreName.APP, 'isOfficial')
},
callback: async (memberList: any[]) => {
if (!memberList || memberList.length === 0) {
@ -107,7 +102,7 @@ export default class TUIConversationServer {
await this.generateConversation(`C2C${userID}`);
this.routerForward(`C2C${userID}`);
}
},
}
});
}
@ -117,7 +112,7 @@ export default class TUIConversationServer {
method: TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP,
params: {
title: TUITranslateService.t('TUIConversation.发起群聊'),
memberList,
memberList
},
callback: async (group: any) => {
let conversationID: string | null = null;
@ -127,18 +122,18 @@ export default class TUIConversationServer {
conversationID = `GROUP${groupID}`;
}
this.routerForward(conversationID);
},
}
});
}
private async routerForward(conversationID: string | null): Promise<void> {
if (isUniFrameWork) {
await TUIGlobal?.reLaunch({
url: '/TUIKit/components/TUIConversation/index',
url: '/TUIKit/components/TUIConversation/index'
});
if (conversationID) {
TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index',
url: '/TUIKit/components/TUIChat/index'
});
}
}

View File

@ -6,36 +6,36 @@ const groupIntroConfig = [
label: '陌生人社交群Public',
type: TUIChatEngine.TYPES.GRP_PUBLIC,
detail: '类似 QQ 群,创建后群主可以指定群管理员,用户搜索群 ID 发起加群申请后,需要群主或管理员审批通过才能入群。详见',
src: '产品文档',
src: '产品文档'
},
{
icon: 'https://web.sdk.qcloud.com/im/assets/images/Meeting.svg',
label: '临时会议群Meeting',
type: TUIChatEngine.TYPES.GRP_MEETING,
detail: '创建后可以随意进出,且支持查看入群前消息;适合用于音视频会议场景、在线教育场景等与实时音视频产品结合的场景。详见',
src: '产品文档',
src: '产品文档'
},
{
icon: 'https://web.sdk.qcloud.com/im/assets/images/Work.svg',
label: '好友工作群Work',
type: TUIChatEngine.TYPES.GRP_WORK,
detail: '类似普通微信群,创建后仅支持已在群内的好友邀请加群,且无需被邀请方同意或群主审批。详见',
src: '产品文档',
src: '产品文档'
},
{
icon: 'https://web.sdk.qcloud.com/im/assets/images/AVChatroom.svg',
label: '直播群AVChatroom',
type: TUIChatEngine.TYPES.GRP_AVCHATROOM,
detail: '创建后可以随意进出,没有群成员数量上限,但不支持历史消息存储;适合与直播产品结合,用于弹幕聊天场景。详见',
src: '产品文档',
src: '产品文档'
},
{
icon: 'https://web.sdk.qcloud.com/im/assets/images/Community.png',
label: '社群Community',
type: TUIChatEngine.TYPES.GRP_COMMUNITY,
detail: '创建后可以随意进出最多支持100000人支持历史消息存储用户搜索群 ID 发起加群申请后,无需管理员审批即可进群。详见',
src: '产品文档',
},
src: '产品文档'
}
];
const findGroupIntroConfig = (type: string) => {
@ -44,7 +44,4 @@ const findGroupIntroConfig = (type: string) => {
})[0];
};
export {
groupIntroConfig,
findGroupIntroConfig,
};
export { groupIntroConfig, findGroupIntroConfig };

View File

@ -1,34 +1,18 @@
<template>
<ul class="group-introduction-list select">
<li
v-for="(item, index) in type"
:key="index"
class="select-item"
:class="[selectType === item.type && 'selected']"
@click="selected(item)"
>
<li v-for="(item, index) in type" :key="index" class="select-item" :class="[selectType === item.type && 'selected']" @click="selected(item)">
<main class="select-item-type">
<div class="select-item-header">
<aside class="left">
<Icon
class="icon"
:file="item.icon"
/>
<Icon class="icon" :file="item.icon" />
<span class="select-item-label">{{ TUITranslateService.t(`TUIGroup.${item.label}`) }}</span>
</aside>
<Icon
v-if="selectType === item.type"
:file="selectedIcon"
/>
<Icon v-if="selectType === item.type" :file="selectedIcon" />
</div>
<span class="select-item-detail">{{ TUITranslateService.t(`TUIGroup.${item.detail}`) }}</span>
<a
class="link"
:href="documentLink.product.url"
target="_blank"
@click="openUrl(documentLink.product.url)"
>{{
TUITranslateService.t(`TUIGroup.${item.src}`) }}</a>
<a class="link" :href="documentLink.product.url" target="_blank" @click="openUrl(documentLink.product.url)">{{
TUITranslateService.t(`TUIGroup.${item.src}`)
}}</a>
</main>
</li>
</ul>
@ -46,8 +30,8 @@ import { isUniFrameWork } from '../../../../utils/env';
const props = defineProps({
groupType: {
type: String,
default: '',
},
default: ''
}
});
const type = groupIntroConfig;
@ -70,6 +54,5 @@ const openUrl = (link: string) => {
TUIGlobal?.open(link);
}
};
</script>
<style lang="scss" scoped src="../style/index.scss"></style>

View File

@ -1,54 +1,23 @@
<template>
<Dialog
:show="true"
:isH5="!isPC"
:isHeaderShow="false"
:isFooterShow="false"
:background="false"
@update:show="closeCreated"
>
<div
class="group"
:class="[!isPC ? 'group-h5' : '']"
>
<Dialog :show="true" :isH5="!isPC" :isHeaderShow="false" :isFooterShow="false" :background="false" @update:show="closeCreated">
<div class="group" :class="[!isPC ? 'group-h5' : '']">
<div class="group-box">
<header class="group-box-header">
<Icon
:file="isPC ? closeIcon : backIcon"
class="icon-close"
size="16px"
@onClick="closeCreated"
/>
<Icon :file="isPC ? closeIcon : backIcon" class="icon-close" size="16px" @onClick="closeCreated" />
<h1 class="group-box-header-title">
{{ headerTitle }}
</h1>
</header>
<ul
v-if="!groupInfo.isEdit"
class="group-list"
>
<ul v-if="!groupInfo.isEdit" class="group-list">
<li class="group-list-item">
<label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群头像') }}</label>
<Avatar :url="groupInfo.profile.avatar" />
</li>
<ul>
<li
v-for="(item, index) in createInfo"
:key="index"
class="group-list-item"
>
<li v-for="(item, index) in createInfo" :key="index" class="group-list-item">
<label class="group-list-item-label">{{ item.name }}</label>
<input
v-if="isPC"
v-model="groupInfo.profile[item.key]"
type="text"
:placeholder="item.placeholder"
>
<span
v-else
class="group-h5-list-item-content"
@click="edit(item.key)"
>
<input v-if="isPC" v-model="groupInfo.profile[item.key]" type="text" :placeholder="item.placeholder" />
<span v-else class="group-h5-list-item-content" @click="edit(item.key)">
<p class="content">{{ groupInfo.profile[item.key] }}</p>
<Icon :file="rightIcon" />
</span>
@ -56,30 +25,16 @@
<li class="group-list-introduction">
<div class="group-list-item">
<label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群类型') }}</label>
<GroupIntroduction
v-if="isPC"
:groupType="groupInfo.profile.type"
@selectType="selected"
/>
<span
v-else
class="group-h5-list-item-content"
@click="edit('type')"
>
<GroupIntroduction v-if="isPC" :groupType="groupInfo.profile.type" @selectType="selected" />
<span v-else class="group-h5-list-item-content" @click="edit('type')">
<p class="content">{{ groupTypeDetail.label }}</p>
<Icon :file="rightIcon" />
</span>
</div>
<article
v-if="!isPC"
class="group-h5-list-item-introduction"
>
<article v-if="!isPC" class="group-h5-list-item-introduction">
<label class="introduction-name">{{ groupTypeDetail.label }}</label>
<span class="introduction-detail">{{ groupTypeDetail.detail }}</span>
<a
:href="documentLink.product.url"
target="view_window"
>
<a :href="documentLink.product.url" target="view_window">
{{ TUITranslateService.t(`TUIGroup.${groupTypeDetail.src}`) }}
</a>
</article>
@ -87,37 +42,21 @@
</ul>
</ul>
<!-- Edit Group Name -->
<div
v-else
class="group-list group-list-edit"
>
<div v-else class="group-list group-list-edit">
<input
v-if="groupInfo.groupConfig.type === 'input'"
v-model="groupInfo.groupConfig.value"
class="group-name-input"
type="text"
:placeholder="TUITranslateService.t(`TUIGroup.${groupInfo.groupConfig.placeholder}`)"
>
<GroupIntroduction
v-else
class="group-introduction-list"
:groupType="groupInfo.groupConfig.value"
@selectType="selected"
/>
<GroupIntroduction v-else class="group-introduction-list" :groupType="groupInfo.groupConfig.value" @selectType="selected" />
</div>
<footer class="group-profile-footer">
<button
v-if="isPC && !groupInfo.isEdit"
class="btn-default"
@click="closeCreated"
>
<button v-if="isPC && !groupInfo.isEdit" class="btn-default" @click="closeCreated">
{{ TUITranslateService.t('TUIGroup.取消') }}
</button>
<button
class="btn-submit"
:disabled="submitDisabledStatus"
@click="submit"
>
<button class="btn-submit" :disabled="submitDisabledStatus" @click="submit">
{{ TUITranslateService.t('TUIGroup.确认') }}
</button>
</footer>
@ -126,12 +65,7 @@
</Dialog>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUITranslateService,
TUIGroupService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUITranslateService, TUIGroupService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { computed, reactive, watchEffect } from '../../../adapter-vue';
import documentLink from '../../../utils/documentLink';
import { isPC } from '../../../utils/env';
@ -159,16 +93,16 @@ const groupInfo = reactive<any>({
notification: '',
// joinOption: '',
memberList: [],
isSupportTopic: false,
isSupportTopic: false
},
groupConfig: {
title: '',
value: '',
key: '',
type: '',
placeholder: '',
placeholder: ''
},
isEdit: false,
isEdit: false
});
watchEffect(() => {
@ -193,16 +127,14 @@ const createInfo = computed(() => {
const groupNameInput = {
name: TUITranslateService.t('TUIGroup.群名称'),
key: 'name',
placeholder: TUITranslateService.t('TUIGroup.请输入群名称'),
placeholder: TUITranslateService.t('TUIGroup.请输入群名称')
};
const groupIDInput = {
name: `${TUITranslateService.t('TUIGroup.群ID')}(${TUITranslateService.t('TUIGroup.选填')})`,
key: 'groupID',
placeholder: TUITranslateService.t('TUIGroup.请输入群ID'),
placeholder: TUITranslateService.t('TUIGroup.请输入群ID')
};
return groupInfo.profile.type === TUIChatEngine.TYPES.GRP_COMMUNITY
? [groupNameInput]
: [groupNameInput, groupIDInput];
return groupInfo.profile.type === TUIChatEngine.TYPES.GRP_COMMUNITY ? [groupNameInput] : [groupNameInput, groupIDInput];
});
const submitDisabledStatus = computed(() => {
@ -232,18 +164,18 @@ const createGroup = async (options: any) => {
if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) {
await TUIGroupService.joinGroup({
groupID: res.data.group.groupID,
applyMessage: '',
applyMessage: ''
});
}
handleCompleteCreate(res.data.group);
Toast({
message: TUITranslateService.t('TUIGroup.群组创建成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
} catch (err: any) {
Toast({
message: err.message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
};
@ -252,7 +184,7 @@ const submit = () => {
const { profile } = groupInfo;
if (groupInfo.isEdit) {
groupInfo.profile[groupInfo.groupConfig.key] = groupInfo.groupConfig.value;
return groupInfo.isEdit = !groupInfo.isEdit;
return (groupInfo.isEdit = !groupInfo.isEdit);
} else {
createGroup(profile);
}
@ -260,7 +192,7 @@ const submit = () => {
const closeCreated = () => {
if (groupInfo.isEdit) {
return groupInfo.isEdit = !groupInfo.isEdit;
return (groupInfo.isEdit = !groupInfo.isEdit);
}
handleCompleteCreate(null);
};
@ -292,6 +224,5 @@ const handleCompleteCreate = (group: any) => {
const callback = TUIGroupServer.getOnCallCallback(TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP);
callback && callback(group);
};
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -6,10 +6,7 @@
</div>
</template>
<script lang="ts" setup>
import {
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../adapter-vue';
import CreateGroup from './create-group/index.vue';
@ -41,9 +38,8 @@ TUIStore.watch(StoreName.GRP, {
} else {
isShowSelectMember.value = false;
}
},
}
});
</script>
<style lang="scss" scoped>
.tui-group {

View File

@ -1,36 +1,16 @@
<template>
<div
ref="manageRef"
class="manage"
>
<header
v-if="!isUniFrameWork || currentTab ==='admin'"
class="manage-header"
>
<Icon
:file="backSVG"
@onClick="back"
/>
<div ref="manageRef" class="manage">
<header v-if="!isUniFrameWork || currentTab === 'admin'" class="manage-header">
<Icon :file="backSVG" @onClick="back" />
<div class="manage-header-content">
{{ TUITranslateService.t(`TUIGroup.${TabName}`) }}
</div>
<div />
</header>
<main
v-if="!currentTab || (isUniFrameWork && currentTab != 'admin')"
class="main"
>
<ManageName
class="space-top"
:isAuthor="isOwner || isAdmin || isWorkGroup"
:data="currentGroup"
@update="updateProfile"
/>
<main v-if="!currentTab || (isUniFrameWork && currentTab != 'admin')" class="main">
<ManageName class="space-top" :isAuthor="isOwner || isAdmin || isWorkGroup" :data="currentGroup" @update="updateProfile" />
<div class="user-info space-top">
<header
class="user-info-header"
@click="setCurrentTab('member')"
>
<header class="user-info-header" @click="setCurrentTab('member')">
<label class="user-info-header-left">
{{ TUITranslateService.t(`TUIGroup.群成员`) }}
</label>
@ -43,143 +23,71 @@
</div>
</header>
<ol class="user-info-list">
<dl
v-for="(item, index) in groupMemberList.slice(0, showUserNum)"
:key="index"
class="user-info-list-item"
>
<dt
class="user-info-list-item-main"
@click="handleMemberProfileShow(item)"
>
<dl v-for="(item, index) in groupMemberList.slice(0, showUserNum)" :key="index" class="user-info-list-item">
<dt class="user-info-list-item-main" @click="handleMemberProfileShow(item)">
<img
class="avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
</dt>
<dd class="user-info-list-item-info">
{{ item.nick || item.userID }}
</dd>
</dl>
<dl
v-if="isShowAddMember"
class="user-info-list-item"
>
<dt
class="avatar"
@click="toggleMask('add')"
>
+
</dt>
<dl v-if="isShowAddMember" class="user-info-list-item">
<dt class="avatar" @click="toggleMask('add')">+</dt>
</dl>
<dl
v-if="currentSelfRole === 'Owner'"
class="user-info-list-item"
>
<dt
class="avatar"
@click="toggleMask('remove')"
>
-
</dt>
<dl v-if="currentSelfRole === 'Owner'" class="user-info-list-item">
<dt class="avatar" @click="toggleMask('remove')">-</dt>
</dl>
</ol>
</div>
<ul
class="content list space-top"
@click="editLableName = ''"
>
<li
class="list-item"
@click="setCurrentTab('notification')"
>
<ul class="content list space-top" @click="editLableName = ''">
<li class="list-item" @click="setCurrentTab('notification')">
<aside class="aside">
<label class="label">{{
TUITranslateService.t(`TUIGroup.群公告`)
}}</label>
<label class="label">{{ TUITranslateService.t(`TUIGroup.群公告`) }}</label>
<article class="article">
{{ currentGroup.notification }}
</article>
</aside>
<Icon
:file="rightIcon"
class="end"
/>
<Icon :file="rightIcon" class="end" />
</li>
<li
v-if="(isAdmin || isOwner) && isSetMuteTime"
class="list-item"
@click="setCurrentTab('admin')"
>
<label class="label">{{
TUITranslateService.t(`TUIGroup.群管理`)
}}</label>
<li v-if="(isAdmin || isOwner) && isSetMuteTime" class="list-item" @click="setCurrentTab('admin')">
<label class="label">{{ TUITranslateService.t(`TUIGroup.群管理`) }}</label>
<Icon :file="rightIcon" />
</li>
<li class="list-item">
<label class="label">{{
TUITranslateService.t(`TUIGroup.群ID`)
}}</label>
<label class="label">{{ TUITranslateService.t(`TUIGroup.群ID`) }}</label>
<div class="groupID">
<span class="span">{{ currentGroupID }}</span>
</div>
</li>
<li class="list-item">
<label class="label">{{
TUITranslateService.t(`TUIGroup.群头像`)
}}</label>
<label class="label">{{ TUITranslateService.t(`TUIGroup.群头像`) }}</label>
<img
class="avatar"
:src="
currentGroup.avatar ||
'https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'
"
:src="currentGroup.avatar || 'https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'"
>
/>
</li>
<li class="list-item">
<label class="label">{{
TUITranslateService.t(`TUIGroup.群类型`)
}}</label>
<span class="span">{{
TUITranslateService.t(`TUIGroup.${typeName[currentGroup.type]}`)
}}</span>
<label class="label">{{ TUITranslateService.t(`TUIGroup.群类型`) }}</label>
<span class="span">{{ TUITranslateService.t(`TUIGroup.${typeName[currentGroup.type]}`) }}</span>
</li>
<li class="list-item">
<label class="label">{{
TUITranslateService.t(`TUIGroup.加群方式`)
}}</label>
<span class="span">{{
TUITranslateService.t(
`TUIGroup.${typeName[currentGroup.joinOption]}`
)
}}</span>
<label class="label">{{ TUITranslateService.t(`TUIGroup.加群方式`) }}</label>
<span class="span">{{ TUITranslateService.t(`TUIGroup.${typeName[currentGroup.joinOption]}`) }}</span>
</li>
</ul>
<ul class="footer list space-top">
<li
v-if="currentSelfRole === 'Owner' && groupMemberList.length > 1"
class="list-item"
@click.stop="toggleMask('changeOwner')"
>
<li v-if="currentSelfRole === 'Owner' && groupMemberList.length > 1" class="list-item" @click.stop="toggleMask('changeOwner')">
{{ TUITranslateService.t(`TUIGroup.转让群组`) }}
</li>
<li
v-if="canIDissmissGroup"
class="list-item"
@click.stop="dismissGroup(currentGroup)"
>
<li v-if="canIDissmissGroup" class="list-item" @click.stop="dismissGroup(currentGroup)">
{{ TUITranslateService.t(`TUIGroup.解散群聊`) }}
</li>
<li
v-else
class="list-item"
@click.stop="quitGroup(currentGroup)"
>
<li v-else class="list-item" @click.stop="quitGroup(currentGroup)">
{{ TUITranslateService.t(`TUIGroup.退出群组`) }}
</li>
</ul>
@ -195,11 +103,7 @@
@handleMemberProfileShow="handleMemberProfileShow"
@close="setCurrentTab('')"
/>
<ManageProfile
v-if="currentTab === 'profile'"
:userInfo="currentMember"
@close="setCurrentTab('')"
/>
<ManageProfile v-if="currentTab === 'profile'" :userInfo="currentMember" @close="setCurrentTab('')" />
<ManageNotification
v-if="currentTab === 'notification'"
:isAuthor="isOwner || isAdmin || isWorkGroup"
@ -220,10 +124,7 @@
@removeMute="toggleMask('removeMute')"
@close="setCurrentTab('')"
/>
<MaskLayer
:show="mask"
@update:show="(e) => (mask = e)"
>
<MaskLayer :show="mask" @update:show="(e) => (mask = e)">
<Transfer
:title="TUITranslateService.t(`TUIGroup.${transferTitle}`)"
:list="transferList"
@ -246,29 +147,17 @@
@submit="handleManage(deletedUserList, 'remove')"
@update:show="(e) => (delDialogShow = e)"
>
<p
v-if="deletedUserList.length === 1"
class="del-dialog-title"
>
<p v-if="deletedUserList.length === 1" class="del-dialog-title">
{{ TUITranslateService.t(`TUIGroup.确定从群聊中删除该成员?`) }}
</p>
<p
v-if="deletedUserList.length > 1"
class="del-dialog-title"
>
<p v-if="deletedUserList.length > 1" class="del-dialog-title">
{{ TUITranslateService.t(`TUIGroup.确定从群聊中删除所选成员?`) }}
</p>
</Dialog>
</div>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watchEffect,
onMounted,
nextTick,
} from '../../../adapter-vue';
import { ref, computed, watchEffect, onMounted, nextTick } from '../../../adapter-vue';
import TUIChatEngine, {
TUITranslateService,
TUIGroupService,
@ -277,7 +166,7 @@ import TUIChatEngine, {
StoreName,
IGroupModel,
TUIConversationService,
IConversationModel,
IConversationModel
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api';
import MaskLayer from '../../common/MaskLayer/index.vue';
@ -303,12 +192,12 @@ const TUIConstants = TUIGroupServer.constants;
const props = defineProps({
groupID: {
type: String,
default: '',
default: ''
},
groupCurrentTab: {
type: String,
default: '',
},
default: ''
}
});
const manageRef = ref<any>(undefined);
@ -319,7 +208,7 @@ const transferType = ref('');
const mask = ref(false);
const currentGroupID = ref('');
const userInfo = ref({
list: [] as IGroupMember[],
list: [] as IGroupMember[]
});
const currentMember = ref<IGroupMember>({});
const typeName = ref({
@ -330,12 +219,12 @@ const typeName = ref({
[TUIChatEngine.TYPES.GRP_COMMUNITY]: '社群',
[TUIChatEngine.TYPES.JOIN_OPTIONS_FREE_ACCESS]: '自由加入',
[TUIChatEngine.TYPES.JOIN_OPTIONS_NEED_PERMISSION]: '需要验证',
[TUIChatEngine.TYPES.JOIN_OPTIONS_DISABLE_APPLY]: '禁止加群',
[TUIChatEngine.TYPES.JOIN_OPTIONS_DISABLE_APPLY]: '禁止加群'
});
const member = ref({
admin: [] as IGroupMember[],
member: [] as IGroupMember[],
muteMember: [] as IGroupMember[],
muteMember: [] as IGroupMember[]
});
const transferList = ref<IGroupMember[]>([]);
const transferTitle = ref('');
@ -354,7 +243,7 @@ onMounted(() => {
if (manageRef.value && !isUniFrameWork) {
outsideClick.listen({
domRefs: manageRef.value,
handler: handleCompleteManage,
handler: handleCompleteManage
});
}
});
@ -372,7 +261,7 @@ TUIStore.watch(StoreName.GRP, {
member.value = {
admin: [],
member: [],
muteMember: [],
muteMember: []
};
Array.from(memberList).map((item: any) => {
switch (item?.role) {
@ -388,16 +277,14 @@ TUIStore.watch(StoreName.GRP, {
return item;
});
const time: number = new Date().getTime();
member.value.muteMember = Array.from(memberList).filter(
(item: any) => item?.muteUntil * 1000 - time > 0,
);
},
member.value.muteMember = Array.from(memberList).filter((item: any) => item?.muteUntil * 1000 - time > 0);
}
});
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
groupIDValue.value = conversation?.groupProfile?.groupID;
},
}
});
watchEffect(() => {
@ -460,7 +347,7 @@ const updateProfile = async (newGroupProfile: any) => {
const { key, value } = newGroupProfile;
const options: any = {
groupID: currentGroup.value.groupID,
[key]: value,
[key]: value
};
TUIGroupService.updateGroupProfile(options)
.then((res: any) => {
@ -470,7 +357,7 @@ const updateProfile = async (newGroupProfile: any) => {
.catch((error: any) => {
Toast({
message: error?.message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
});
};
@ -504,9 +391,7 @@ const toggleMask = async (type?: string) => {
break;
case 'remove':
isRadio.value = false;
transferList.value = groupMemberList.value.filter(
(item: any) => item.userID !== currentGroup?.value?.selfInfo?.userID,
);
transferList.value = groupMemberList.value.filter((item: any) => item.userID !== currentGroup?.value?.selfInfo?.userID);
transferTitle.value = '删除成员';
break;
case 'addAdmin':
@ -550,22 +435,14 @@ const toggleMask = async (type?: string) => {
const friendList = async () => {
const imResponse = await TUIFriendService.getFriendList();
const friendList = imResponse.data.map((item: any) => item?.profile);
return friendList.filter(
(item: any) =>
!userInfo.value.list.some(
(infoItem: any) => infoItem.userID === item.userID,
),
);
return friendList.filter((item: any) => !userInfo.value.list.some((infoItem: any) => infoItem.userID === item.userID));
};
const canIDissmissGroup = computed(() => {
const userRole = currentGroup?.value?.selfInfo?.role;
const groupType = currentGroup?.value?.type;
return (
userRole === TUIChatEngine.TYPES.GRP_MBR_ROLE_OWNER
&& groupType !== TUIChatEngine.TYPES.GRP_WORK
);
return userRole === TUIChatEngine.TYPES.GRP_MBR_ROLE_OWNER && groupType !== TUIChatEngine.TYPES.GRP_WORK;
});
const isShowAddMember = computed(() => {
@ -589,7 +466,7 @@ const getMember = async (type?: string) => {
const options = {
groupID,
count: 100,
offset: type && type === 'more' ? userInfo.value.list.length : 0,
offset: type && type === 'more' ? userInfo.value.list.length : 0
};
await TUIGroupService.getGroupMemberList(options).then((res: any) => {
if (type && type === 'more') {
@ -620,7 +497,7 @@ const dismissGroup = async (group: any) => {
enableSampleTaskStatus('dismissGroup');
Toast({
message: TUITranslateService.t('TUIGroup.群组解散成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
clearGroupInfo();
};
@ -628,7 +505,7 @@ const dismissGroup = async (group: any) => {
const clearGroupInfo = () => {
if (isUniFrameWork) {
TUIGlobal?.switchTab({
url: '/TUIKit/components/TUIConversation/index',
url: '/TUIKit/components/TUIConversation/index'
});
} else {
handleCompleteManage();
@ -643,12 +520,12 @@ const setAllMuteTime = (value: boolean) => {
enableSampleTaskStatus('muteGroup');
Toast({
message: TUITranslateService.t('TUIGroup.禁言设置成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
} else {
Toast({
message: TUITranslateService.t('TUIGroup.取消禁言成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
}
};
@ -658,15 +535,13 @@ const handleSearchMember = async (value: string) => {
let imMemberResponse: any = {};
const options: any = {
groupID: currentGroupID.value,
userIDList: [value],
userIDList: [value]
};
switch (transferType.value) {
case 'add':
try {
imMemberResponse = await TUIGroupService.getGroupMemberProfile(options);
transferList.value = transferList.value.filter(
(item: any) => item.userID !== imResponse.data[0]?.userID,
);
transferList.value = transferList.value.filter((item: any) => item.userID !== imResponse.data[0]?.userID);
transferList.value = [...transferList.value, ...imResponse.data];
if (imMemberResponse?.data?.memberList.length > 0) {
transferList.value = transferList.value.map((item: any) => {
@ -680,7 +555,7 @@ const handleSearchMember = async (value: string) => {
const message = TUITranslateService.t('TUIGroup.该用户不存在');
Toast({
message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
break;
@ -691,23 +566,18 @@ const handleSearchMember = async (value: string) => {
const message = TUITranslateService.t('TUIGroup.该用户不在群组内');
Toast({
message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
transferList.value = transferList.value.filter(
(item: any) => item.userID !== imResponse?.data?.memberList[0]?.userID,
);
transferList.value = transferList.value.filter((item: any) => item.userID !== imResponse?.data?.memberList[0]?.userID);
if (imResponse?.data?.memberList.length) {
transferList.value = [
...transferList.value,
...imResponse.data.memberList,
];
transferList.value = [...transferList.value, ...imResponse.data.memberList];
}
} catch (error: any) {
const message = TUITranslateService.t('TUIGroup.该用户不存在');
Toast({
message,
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
}
break;
@ -752,7 +622,7 @@ const handleManage = (userList: any, type: any) => {
const addMember = async (userIDList: any) => {
const options: any = {
groupID: currentGroupID.value,
userIDList,
userIDList
};
await TUIGroupService.addGroupMember(options);
};
@ -760,7 +630,7 @@ const addMember = async (userIDList: any) => {
const changeOwner = async (userID: any) => {
const options: any = {
groupID: currentGroupID.value,
newOwnerID: userID,
newOwnerID: userID
};
const imResponse = await TUIGroupService.changeGroupOwner(options);
currentGroup.value = {};
@ -771,7 +641,7 @@ const setMemberMuteTime = async (userID: string, type?: string) => {
const options: any = {
groupID: currentGroupID.value,
userID,
muteTime: type === 'add' ? 60 * 60 * 24 * 30 : 0,
muteTime: type === 'add' ? 60 * 60 * 24 * 30 : 0
};
await TUIGroupService.setGroupMemberMuteTime(options);
};
@ -789,7 +659,7 @@ const handleAdmin = async (user: any) => {
const options: any = {
groupID: currentGroupID.value,
userID: user.userID,
role,
role
};
await TUIGroupService.setGroupMemberRole(options);
};
@ -798,7 +668,7 @@ const deleteGroupMember = async (userIDList: any) => {
const options: any = {
groupID: currentGroupID.value,
userIDList,
reason: '',
reason: ''
};
await TUIGroupService.deleteGroupMember(options);
};

View File

@ -5,123 +5,66 @@
{{ TUITranslateService.t(`TUIGroup.群管理员`) }}
</div>
<ul class="admin-manage-list">
<li
v-for="(item, index) in memberAdmin.admin"
:key="index"
class="admin-manage-list-item"
>
<li v-for="(item, index) in memberAdmin.admin" :key="index" class="admin-manage-list-item">
<div class="item-main">
<img
class="item-main-avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
</div>
<div class="item-name">
{{ item.nick || item.userID }}
</div>
</li>
<li class="admin-manage-list-item">
<div
class="item-main"
@click="addAdmin"
>
<Icon
:file="plusSVG"
width="16px"
height="16px"
/>
<div class="item-main" @click="addAdmin">
<Icon :file="plusSVG" width="16px" height="16px" />
</div>
</li>
<li class="admin-manage-list-item">
<div
v-if="memberAdmin.admin.length > 0"
class="item-main"
@click="removeAdmin"
>
<Icon
:file="minusSVG"
width="16px"
height="16px"
/>
<div v-if="memberAdmin.admin.length > 0" class="item-main" @click="removeAdmin">
<Icon :file="minusSVG" width="16px" height="16px" />
</div>
</li>
</ul>
</div>
<div
v-if="isAdminSetMuteTime"
class="admin-mute-all"
>
<div v-if="isAdminSetMuteTime" class="admin-mute-all">
<div>
<div class="admin-mute-all-title">
{{ TUITranslateService.t(`TUIGroup.全员禁言`) }}
</div>
<div class="admin-mute-all-content">
{{
TUITranslateService.t(
`TUIGroup.全员禁言开启后,只允许群主和管理员发言。`
)
}}
{{ TUITranslateService.t(`TUIGroup.全员禁言开启后,只允许群主和管理员发言。`) }}
</div>
</div>
<Slider
:open="currentGroupAdmin.muteAllMembers"
@change="setAllMuteTime"
/>
<Slider :open="currentGroupAdmin.muteAllMembers" @change="setAllMuteTime" />
</div>
<div
v-if="isAdminSetMuteTime"
class="admin-mute"
>
<div v-if="isAdminSetMuteTime" class="admin-mute">
<div class="admin-mute-header">
{{ TUITranslateService.t(`TUIGroup.单独禁言人员`) }}
</div>
<ul class="admin-mute-list">
<li
v-for="(item, index) in memberAdmin.muteMember"
:key="index"
class="admin-mute-list-item"
>
<li v-for="(item, index) in memberAdmin.muteMember" :key="index" class="admin-mute-list-item">
<div class="item-main">
<img
class="item-main-avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
</div>
<div class="item-name">
{{ item.nick || item.userID }}
</div>
</li>
<li class="admin-mute-list-item">
<div
class="item-main"
@click="addMute"
>
<Icon
:file="plusSVG"
width="16px"
height="16px"
/>
<div class="item-main" @click="addMute">
<Icon :file="plusSVG" width="16px" height="16px" />
</div>
</li>
<li class="admin-mute-list-item">
<div
v-if="memberAdmin.muteMember.length > 0"
class="item-main"
@click="removeMute"
>
<Icon
:file="minusSVG"
width="16px"
height="16px"
/>
<div v-if="memberAdmin.muteMember.length > 0" class="item-main" @click="removeMute">
<Icon :file="minusSVG" width="16px" height="16px" />
</div>
</li>
</ul>
@ -130,10 +73,7 @@
</template>
<script lang="ts" setup>
import {
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue';
import Slider from '../../common/Slider/index.vue';
import Icon from '../../common/Icon.vue';
@ -144,23 +84,23 @@ import { IGroupMember } from '../../../interface';
const props = defineProps({
member: {
type: Object,
default: () => {},
default: () => {}
},
isSetMuteTime: {
type: Boolean,
default: () => false,
default: () => false
},
currentGroup: {
type: Object,
default: () => {},
},
default: () => {}
}
});
const isAdminSetMuteTime = ref(false);
const memberAdmin = ref({
admin: [] as Array<IGroupMember>,
member: [] as Array<IGroupMember>,
muteMember: [] as Array<IGroupMember>,
muteMember: [] as Array<IGroupMember>
});
const currentGroupAdmin = ref<IGroupModel>();
@ -174,14 +114,7 @@ watchEffect(() => {
currentGroupAdmin.value = props.currentGroup;
});
const emits = defineEmits([
'addAdmin',
'removeAdmin',
'setAllMuteTime',
'addMute',
'removeMute',
'close',
]);
const emits = defineEmits(['addAdmin', 'removeAdmin', 'setAllMuteTime', 'addMute', 'removeMute', 'close']);
const addAdmin = () => {
emits('addAdmin');
@ -205,7 +138,7 @@ const removeMute = () => {
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.admin {
width: 100%;
@ -218,7 +151,7 @@ const removeMute = () => {
padding: 10px;
&-left {
font-family: "PingFang SC", sans-serif;
font-family: 'PingFang SC', sans-serif;
font-size: 18px;
font-weight: 500;
line-height: 50px;
@ -227,7 +160,7 @@ const removeMute = () => {
}
&-close {
font-family: "PingFang SC", sans-serif;
font-family: 'PingFang SC', sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 48px;
@ -253,7 +186,7 @@ const removeMute = () => {
&-header {
padding-left: 10px;
font-family: "PingFang SC", sans-serif;
font-family: 'PingFang SC', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 20px;
@ -314,7 +247,7 @@ const removeMute = () => {
&-title {
padding-left: 10px;
font-family: "PingFang SC", sans-serif;
font-family: 'PingFang SC', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 20px;
@ -325,7 +258,7 @@ const removeMute = () => {
&-content {
color: #999;
padding-left: 10px;
font-family: "PingFang SC", sans-serif;
font-family: 'PingFang SC', sans-serif;
font-size: 12px;
font-weight: 400;
line-height: 17px;

View File

@ -1,99 +1,50 @@
<template>
<main
v-if="!isUniFrameWork"
class="member"
>
<main v-if="!isUniFrameWork" class="member">
<ul class="list">
<li
v-for="(item, index) in memberList"
:key="index"
class="list-item"
>
<aside
class="aside"
@click="handleMemberProfileShow(item)"
>
<li v-for="(item, index) in memberList" :key="index" class="list-item">
<aside class="aside" @click="handleMemberProfileShow(item)">
<img
class="avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
<span class="name">{{ item.nick || item.userID }}</span>
<span>{{ handleRoleName(item) }}</span>
</aside>
<div @click="submit(item)">
<Icon
v-if="item.role !== 'Owner' && isShowDeleteBtn"
:file="delIcon"
:width="'16px'"
:height="'16px'"
/>
<Icon v-if="item.role !== 'Owner' && isShowDeleteBtn" :file="delIcon" :width="'16px'" :height="'16px'" />
</div>
</li>
<li
v-if="memberList.length < totalMember"
class="list-item"
@click="getMore"
>
<li v-if="memberList.length < totalMember" class="list-item" @click="getMore">
{{ TUITranslateService.t(`TUIGroup.查看更多`) }}
</li>
</ul>
</main>
<div
v-else
class="edit-h5"
>
<div v-else class="edit-h5">
<main class="main">
<header class="edit-h5-header">
<aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1>
</aside>
<span
class="close"
@click="close('member')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
<span class="close" @click="close('member')">{{ TUITranslateService.t(`关闭`) }}</span>
</header>
<div class="member">
<ul class="list list-uniapp">
<li
v-for="(item, index) in memberList"
:key="index"
class="list-item"
>
<aside
class="aside"
@click="handleMemberProfileShow(item)"
>
<li v-for="(item, index) in memberList" :key="index" class="list-item">
<aside class="aside" @click="handleMemberProfileShow(item)">
<img
class="avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
<span class="name">{{ item.nick || item.userID }}</span>
<span>{{ handleRoleName(item) }}</span>
</aside>
<div @click="submit(item)">
<Icon
v-if="item.role !== 'Owner' && isShowDeleteBtn"
:file="delIcon"
:width="'16px'"
:height="'16px'"
/>
<Icon v-if="item.role !== 'Owner' && isShowDeleteBtn" :file="delIcon" :width="'16px'" :height="'16px'" />
</div>
</li>
<li
v-if="memberList.length < totalMember"
class="list-item"
@click="getMore"
>
<li v-if="memberList.length < totalMember" class="list-item" @click="getMore">
{{ TUITranslateService.t(`TUIGroup.查看更多`) }}
</li>
</ul>
@ -103,9 +54,7 @@
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatEngine, { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue';
import Icon from '../../common/Icon.vue';
import delIcon from '../../../assets/icon/del-icon.svg';
@ -115,20 +64,20 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({
list: {
type: Array,
default: () => [],
default: () => []
},
total: {
type: Number,
default: () => 0,
default: () => 0
},
isShowDel: {
type: Boolean,
default: () => false,
default: () => false
},
self: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const totalMember = ref(0);
@ -182,7 +131,7 @@ const close = (tabName: string) => {
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.member {
flex: 1;

View File

@ -4,83 +4,38 @@
<div
v-if="isEdit"
:class="{
'edit-h5': isMobile,
'edit-h5': isMobile
}"
>
<main class="edit-h5-main">
<header
v-if="!isPC"
class="edit-h5-header"
>
<header v-if="!isPC" class="edit-h5-header">
<aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.修改群聊名称`) }}</h1>
<span>{{
TUITranslateService.t(
`TUIGroup.修改群聊名称后,将在群内通知其他成员`
)
}}</span>
<span>{{ TUITranslateService.t(`TUIGroup.修改群聊名称后,将在群内通知其他成员`) }}</span>
</aside>
<span
class="close"
@click="toggleEditStatus"
>{{
TUITranslateService.t(`关闭`)
}}</span>
<span class="close" @click="toggleEditStatus">{{ TUITranslateService.t(`关闭`) }}</span>
</header>
<div class="input-box">
<input
v-if="isEdit"
ref="nameInputRef"
v-model="inputGroupName"
class="input"
type="text"
@blur="updateProfile"
>
<span
v-if="!isPC"
class="tip"
>{{
TUITranslateService.t(
`TUIGroup.仅限中文、字母、数字和下划线2-20个字`
)
}}</span>
<input v-if="isEdit" ref="nameInputRef" v-model="inputGroupName" class="input" type="text" @blur="updateProfile" />
<span v-if="!isPC" class="tip">{{ TUITranslateService.t(`TUIGroup.仅限中文字母数字和下划线2-20个字`) }}</span>
</div>
<footer
v-if="!isPC"
class="edit-h5-footer"
>
<button
class="btn"
@click="updateProfile"
>
<footer v-if="!isPC" class="edit-h5-footer">
<button class="btn" @click="updateProfile">
{{ TUITranslateService.t(`确认`) }}
</button>
</footer>
</main>
</div>
<p
v-if="!isEdit || !isPC"
class="name"
@click="toggleEditStatus"
>
<p v-if="!isEdit || !isPC" class="name" @click="toggleEditStatus">
<span>{{ groupProfile.name }}</span>
<Icon
v-if="isAuthor"
class="icon"
:file="editIcon"
width="14px"
height="14px"
/>
<Icon v-if="isAuthor" class="icon" :file="editIcon" width="14px" height="14px" />
</p>
</div>
</template>
<script lang="ts" setup>
import { watchEffect, ref, nextTick, watch } from '../../../adapter-vue';
import {
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
import Icon from '../../common/Icon.vue';
import editIcon from '../../../assets/icon/edit.svg';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
@ -89,12 +44,12 @@ import { isMobile, isPC } from '../../../utils/env';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
default: () => ({})
},
isAuthor: {
type: Boolean,
default: false,
},
default: false
}
});
const groupProfile = ref<IGroupModel>({});
@ -111,7 +66,7 @@ const updateProfile = () => {
if (!inputGroupName.value) {
Toast({
message: TUITranslateService.t('TUIGroup.群名称不能为空'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
} else {
if (inputGroupName.value !== groupProfile.value.name) {
@ -120,7 +75,7 @@ const updateProfile = () => {
inputGroupName.value = '';
Toast({
message: TUITranslateService.t('TUIGroup.群名称修改成功'),
type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS
});
}
toggleEditStatus();
@ -144,12 +99,12 @@ watch(
nameInputRef.value?.focus();
});
}
},
}
);
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.group-name {
padding: 14px 20px;

View File

@ -1,14 +1,6 @@
<template>
<main
v-if="!isUniFrameWork"
class="notification"
>
<textarea
v-if="isEdit"
v-model="input"
class="textarea"
@keyup.enter="updateProfile"
/>
<main v-if="!isUniFrameWork" class="notification">
<textarea v-if="isEdit" v-model="input" class="textarea" @keyup.enter="updateProfile" />
<section v-else>
<p v-if="!groupProfile.notification">
{{ TUITranslateService.t(`TUIGroup.暂无公告`) }}
@ -18,75 +10,37 @@
</article>
</section>
<footer v-if="isAuthorNotification">
<button
v-if="isEdit"
class="btn"
@click="updateProfile"
>
<button v-if="isEdit" class="btn" @click="updateProfile">
{{ TUITranslateService.t(`TUIGroup.发布`) }}
</button>
<button
v-else
class="btn"
@click="isEdit = !isEdit"
>
<button v-else class="btn" @click="isEdit = !isEdit">
{{ TUITranslateService.t(`TUIGroup.编辑`) }}
</button>
</footer>
</main>
<div
v-else
class="edit-h5"
>
<div v-else class="edit-h5">
<main class="edit-h5-main">
<header class="edit-h5-header">
<aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群公告`) }}</h1>
</aside>
<span
class="close"
@click="close('notification')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
<span class="close" @click="close('notification')">{{ TUITranslateService.t(`关闭`) }}</span>
</header>
<div class="notification">
<textarea
v-if="isEdit"
v-model="input"
:class="[isUniFrameWork ? 'uni-height' : '', 'textarea']"
@keyup.enter="updateProfile"
/>
<section
v-else
class="row"
>
<p
v-if="!groupProfile.notification"
class="row-p"
>
<textarea v-if="isEdit" v-model="input" :class="[isUniFrameWork ? 'uni-height' : '', 'textarea']" @keyup.enter="updateProfile" />
<section v-else class="row">
<p v-if="!groupProfile.notification" class="row-p">
{{ TUITranslateService.t(`TUIGroup.暂无公告`) }}
</p>
<article v-else>
{{ groupProfile.notification }}
</article>
</section>
<footer
v-if="isAuthorNotification"
class="footer"
>
<button
v-if="isEdit"
class="btn"
@click="updateProfile"
>
<footer v-if="isAuthorNotification" class="footer">
<button v-if="isEdit" class="btn" @click="updateProfile">
{{ TUITranslateService.t(`TUIGroup.发布`) }}
</button>
<button
v-else
class="btn"
@click="isEdit = !isEdit"
>
<button v-else class="btn" @click="isEdit = !isEdit">
{{ TUITranslateService.t(`TUIGroup.编辑`) }}
</button>
</footer>
@ -97,10 +51,7 @@
<script lang="ts" setup>
import { nextTick } from '../../../adapter-vue';
import {
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import { isUniFrameWork } from '../../../utils/env';
@ -108,12 +59,12 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
default: () => ({})
},
isAuthor: {
type: Boolean,
default: false,
},
default: false
}
});
const groupProfile = ref<IGroupModel>({});
@ -133,7 +84,7 @@ const updateProfile = () => {
if (input.value.length > 150) {
Toast({
message: TUITranslateService.t('TUIGroup.群公告字数超出限制最大长度为150'),
type: TOAST_TYPE.ERROR,
type: TOAST_TYPE.ERROR
});
return;
}
@ -152,7 +103,7 @@ const close = (tabName: string) => {
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.notification {
flex: 1;

View File

@ -1,17 +1,11 @@
<template>
<div
v-if="!isUniFrameWork"
class="memeber-profile"
>
<div v-if="!isUniFrameWork" class="memeber-profile">
<div class="memeber-profile-main">
<img
class="avatar"
:src="
userInfoManager.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
:src="userInfoManager.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
>
/>
<ul class="list">
<h2>{{ userInfoManager.nick || userInfoManager.userID }}</h2>
<li>
@ -19,44 +13,28 @@
<span>{{ userInfoManager.userID }}</span>
</li>
<li>
<label>{{ TUITranslateService.t("TUIContact.个性签名") }}</label>
<label>{{ TUITranslateService.t('TUIContact.个性签名') }}</label>
<span>{{ userInfoManager.selfSignature }}</span>
</li>
</ul>
</div>
<div class="memeber-profile-footer">
<div
v-if="showEnter()"
class="button"
@click="enter(userInfoManager.userID, 'C2C')"
>
{{ TUITranslateService.t("TUIContact.发送消息") }}
<div v-if="showEnter()" class="button" @click="enter(userInfoManager.userID, 'C2C')">
{{ TUITranslateService.t('TUIContact.发送消息') }}
</div>
</div>
</div>
<div
v-else
class="edit-h5"
>
<div v-else class="edit-h5">
<main class="main">
<header class="edit-h5-header">
<aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1>
</aside>
<span
class="close"
@click="close('profile')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
<span class="close" @click="close('profile')">{{ TUITranslateService.t(`关闭`) }}</span>
</header>
<div class="edit-h5-profile">
<div class="memeber-profile-main">
<Avatar
class="avatar"
:url="userInfoManager.avatar"
size="60px"
/>
<Avatar class="avatar" :url="userInfoManager.avatar" size="60px" />
<ul class="list">
<h1>{{ userInfoManager.nick || userInfoManager.userID }}</h1>
<li>
@ -64,18 +42,14 @@
<span>{{ userInfoManager.userID }}</span>
</li>
<li>
<label>{{ TUITranslateService.t("TUIContact.个性签名") }}</label>
<label>{{ TUITranslateService.t('TUIContact.个性签名') }}</label>
<span>{{ userInfoManager.selfSignature }}</span>
</li>
</ul>
</div>
<div class="memeber-profile-footer">
<div
v-if="showEnter()"
class="button"
@click="enter(userInfoManager.userID, 'C2C')"
>
{{ TUITranslateService.t("TUIContact.发送消息") }}
<div v-if="showEnter()" class="button" @click="enter(userInfoManager.userID, 'C2C')">
{{ TUITranslateService.t('TUIContact.发送消息') }}
</div>
</div>
</div>
@ -90,7 +64,7 @@ import TUIChatEngine, {
TUIConversationService,
TUIFriendService,
TUIStore,
StoreName,
StoreName
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import Avatar from '../../common/Avatar/index.vue';
@ -100,8 +74,8 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({
userInfo: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
});
const isFriendShip = ref(false);
@ -110,26 +84,22 @@ const userInfoManager = ref<IUserProfile>({});
watchEffect(() => {
userInfoManager.value = props.userInfo;
});
const emits = defineEmits([
'handleSwitchConversation',
'close',
'openConversation',
]);
const emits = defineEmits(['handleSwitchConversation', 'close', 'openConversation']);
watch(
() => props.userInfo,
async (newVal: any, oldVal: any) => {
if (newVal === oldVal) return;
const res = await TUIUserService.getUserProfile({
userIDList: [props.userInfo.userID],
userIDList: [props.userInfo.userID]
});
userInfoManager.value = res?.data[0];
checkFriend();
},
{
deep: true,
immediate: true,
},
immediate: true
}
);
const enter = async (ID: any, type: string) => {
@ -151,10 +121,10 @@ const checkFriend = async () => {
if (!(userInfoManager.value as any).userID) return;
TUIFriendService.checkFriend({
userIDList: [userInfoManager.value.userID],
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH,
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH
}).then((res: any) => {
const relation = res?.data?.successUserIDList?.[0]?.relation;
isFriendShip.value = (relation === TUIChatEngine.TYPES.SNS_TYPE_BOTH_WAY);
isFriendShip.value = relation === TUIChatEngine.TYPES.SNS_TYPE_BOTH_WAY;
});
};
@ -167,7 +137,7 @@ const close = (tabName: string) => {
};
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.memeber-profile {
flex: 1;
@ -267,7 +237,6 @@ const close = (tabName: string) => {
.avatar {
margin: 20px;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More