发送红包接口有问题,添加群恢复

This commit is contained in:
bobobobo
2026-01-08 01:44:39 +08:00
parent 1634425c17
commit c0619ea4ec
20 changed files with 2070 additions and 1531 deletions

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767794579271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17300" width="80" height="80" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M792.7 960.2H232.6c-22.1 0-40-17.9-40-40v-816c0-22.1 17.9-40 40-40h560.1c22.1 0 40 17.9 40 40v816c0 22.1-17.9 40-40 40z" fill="#CE302F" p-id="17301"></path><path d="M792.7 965.2H232.6c-24.8 0-45-20.2-45-45v-816c0-24.8 20.2-45 45-45h560.1c24.8 0 45 20.2 45 45v816c0 24.8-20.2 45-45 45z m-560.1-896c-19.3 0-35 15.7-35 35v816c0 19.3 15.7 35 35 35h560.1c19.3 0 35-15.7 35-35v-816c0-19.3-15.7-35-35-35H232.6z" fill="#333333" p-id="17302"></path><path d="M827.1 336.5S640 403.9 511.9 404.1c-125.7 0.2-313.3-67.6-313.3-67.6V87c0-12.6 9.4-22.7 21-22.7h586.5c11.6 0 21 10.2 21 22.7v249.5z" fill="#C12727" p-id="17303"></path><path d="M832.7 320.2s-190.6 63.4-321.1 63.6c-128 0.2-319.1-63.6-319.1-63.6V85.6c0-11.8 9.6-21.4 21.4-21.4h597.4c11.8 0 21.4 9.6 21.4 21.4v234.6z" fill="#CE302F" p-id="17304"></path><path d="M511 388.8c-127.3 0-318.1-63.2-320-63.8l-3.4-1.1V85.6c0-14.5 11.8-26.4 26.4-26.4h597.4c14.5 0 26.4 11.8 26.4 26.4v238.2l-3.4 1.1c-1.9 0.6-192.8 63.6-322.6 63.8-0.3 0.1-0.6 0.1-0.8 0.1z m-313.4-72.2c26.2 8.5 197.1 62.2 313.4 62.2h0.6c118.7-0.2 289.8-53.7 316.1-62.2v-231c0-9-7.3-16.4-16.4-16.4H214c-9 0-16.4 7.3-16.4 16.4v231z" fill="#333333" p-id="17305"></path><path d="M512.7 394.8m-97.1 0a97.1 97.1 0 1 0 194.2 0 97.1 97.1 0 1 0-194.2 0Z" fill="#C12727" p-id="17306"></path><path d="M512.7 383.8m-90.7 0a90.7 90.7 0 1 0 181.4 0 90.7 90.7 0 1 0-181.4 0Z" fill="#F8B739" p-id="17307"></path><path d="M512.7 479.5c-52.8 0-95.7-42.9-95.7-95.7s42.9-95.7 95.7-95.7c52.8 0 95.7 42.9 95.7 95.7s-43 95.7-95.7 95.7z m0-181.4c-47.2 0-85.7 38.4-85.7 85.7s38.4 85.7 85.7 85.7c47.2 0 85.7-38.4 85.7-85.7s-38.5-85.7-85.7-85.7z" fill="#333333" p-id="17308"></path><path d="M512.7 372.8m-74.3 0a74.3 74.3 0 1 0 148.6 0 74.3 74.3 0 1 0-148.6 0Z" fill="#F7B034" p-id="17309"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -288,14 +288,15 @@
if (!conversation) { if (!conversation) {
return return
} }
// return when currentConversationID.value is the same as conversation.conversationID. // return when currentConversationID.value is the same as conversation.conversationID.
if (currentConversationID.value === conversation?.conversationID) { if (currentConversationID.value === conversation?.conversationID) {
return return
} }
isGroup.value = false isGroup.value = false
let conversationType = TUIChatEngine.TYPES.CONV_C2C let conversationType = TUIChatEngine.TYPES.CONV_C2C
const conversationID = conversation.conversationID const conversationID = conversation.conversationID
if (conversationID.startsWith(TUIChatEngine.TYPES.CONV_GROUP)) { if (conversationID.startsWith(TUIChatEngine.TYPES.CONV_GROUP)) {
conversationType = TUIChatEngine.TYPES.CONV_GROUP conversationType = TUIChatEngine.TYPES.CONV_GROUP
isGroup.value = true isGroup.value = true
@@ -304,7 +305,6 @@
'' ''
) )
} }
headerExtensionList.value = [] headerExtensionList.value = []
isMultipleSelectMode.value = false isMultipleSelectMode.value = false
// Initialize chatType // Initialize chatType
@@ -350,6 +350,4 @@
} }
</script> </script>
<style scoped lang="scss" src="./style/index.scss"> <style scoped lang="scss" src="./style/index.scss"></style>
</style>

View File

@@ -213,7 +213,6 @@
) )
} }
TUIChatService.sendCustomMessage( TUIChatService.sendCustomMessage(
options as SendMessageParams, options as SendMessageParams,
sendMessageOptions sendMessageOptions

View File

@@ -59,10 +59,10 @@
v-if="featureConfig.InputQuickReplies" v-if="featureConfig.InputQuickReplies"
@onDialogPopupShowOrHide="handleSwiperDotShow" @onDialogPopupShowOrHide="handleSwiperDotShow"
/> />
<Evaluate <!-- <Evaluate
v-if="featureConfig.InputEvaluation" v-if="featureConfig.InputEvaluation"
@onDialogPopupShowOrHide="handleSwiperDotShow" @onDialogPopupShowOrHide="handleSwiperDotShow"
/> /> -->
<!-- 红包 --> <!-- 红包 -->
<RedEnvelope /> <RedEnvelope />
</template> </template>

View File

@@ -3,17 +3,50 @@
import ToolbarItemContainer from '../toolbar-item-container/index.vue' import ToolbarItemContainer from '../toolbar-item-container/index.vue'
import custom from '../../../../assets/icon/red-packet.svg' import custom from '../../../../assets/icon/red-packet.svg'
import { isUniFrameWork } from '../../../../utils/env' import { isUniFrameWork } from '../../../../utils/env'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../constant'
import TUIChatEngine, {
type IConversationModel,
type SendMessageParams,
SendMessageOptions,
StoreName,
TUIChatService,
TUIStore
} from '@tencentcloud/chat-uikit-engine-lite'
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils'
import OfflinePushInfoManager, {
type IOfflinePushInfoCreateParams
} from '../../offlinePushInfoManager'
import { getUserPayPwd } from '@/api/my-index'
import { useUI } from '@/utils/use-ui'
import { navigateTo } from '@/utils/router'
import { sendRedEnvelope } from '../../../../../api/tui-kit'
const placeholderStyle = `font-family: PingFang SC, PingFang SC; font-weight: 500; color: #666666; font-size: 32rpx; font-style: normal; text-transform: none;` const { showDialog } = useUI()
const placeholderStyle = `font-family: PingFang SC, PingFang SC; font-weight: 500; color: #a9a9a9; font-size: 32rpx; font-style: normal; text-transform: none;`
const evaluateIcon = custom const evaluateIcon = custom
/** 提示框 */ const currentConversation = ref<IConversationModel>()
const message = ref()
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation
}
})
const tixian = ref()
/** 错误提示 */
const errorData = reactive({
integralShow: false,
numShow: false,
color: '#f56c6c'
})
const container = ref() const container = ref()
const formData = reactive({ const formData = reactive({
// 积分 // 积分
integral: '', integral: '',
// 红包数量
num: '',
// 红包标题 // 红包标题
title: '' title: ''
}) })
@@ -27,16 +60,49 @@
if (!formData.integral) { if (!formData.integral) {
data.valid = true data.valid = true
data.message = '请输入积分' data.message = '请输入积分'
message.value.open()
return data return data
} }
if (Number(formData.integral) > 2000) { if (Number(formData.integral) > 2000) {
data.valid = true data.valid = true
data.message = '积分不能大于2000' data.message = '积分不能大于2000'
message.value.open()
return data
}
return data
})
/** 监听是否是群组 */
const isGroup = computed(
() => currentConversation?.value?.type === 'GROUP'
)
/** 监听群人数 */
const memberCount = computed(
() => currentConversation?.value?.groupProfile?.memberCount
)
/** 监听红包个数 */
const monitorNum = computed(() => {
const data = {
valid: false,
message: ''
}
if (!formData.num) {
data.valid = true
data.message = '请输入红包个数'
return data return data
} }
if (Number(formData.num) === 0) {
data.valid = true
data.message = '红包个数不能为 0'
return data
}
if (Number(formData.num) > memberCount.value) {
data.valid = true
data.message = '红包个数不能大于群人数'
return data
}
return data return data
}) })
@@ -44,6 +110,9 @@
console.log('弹出窗口') console.log('弹出窗口')
formData.integral = '' formData.integral = ''
formData.title = '' formData.title = ''
formData.num = ''
errorData.integralShow = false
errorData.numShow = false
} }
const onDialogClose = () => { const onDialogClose = () => {
@@ -54,9 +123,106 @@
container?.value?.toggleDialogDisplay(false) container?.value?.toggleDialogDisplay(false)
} }
const onSubmit = () => { /** 监听输入 */
if (!formData.integral) return const monitorInput = () => {
console.log('确认') if (monitorPoints.value.valid) {
errorData.integralShow = true
return
}
errorData.integralShow = false
}
/** 监听红包个数输入值 */
const numInput = () => {
if (monitorNum.value.valid) {
errorData.numShow = true
return
}
errorData.numShow = false
}
const onSubmit = async () => {
if (isGroup.value) {
if (monitorNum.value.valid) {
errorData.numShow = true
return
}
}
if (monitorPoints.value.valid) {
errorData.integralShow = true
return
}
const res = await getUserPayPwd()
if (res?.data) {
tixian.value.open()
} else {
const show = await showDialog('提示', '请先设置支付密码')
if (show) {
navigateTo('/pages/my-index/wallet/edit-password', { type: 0 })
}
}
}
const pwdModalSubmit = async (e: number[]) => {
// 默认文本
const text = `${formData.title || '恭喜发财,大吉大利'}`
const payload = {
data: JSON.stringify({
businessID: CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE,
version: 1,
// 积分
integral: Number(formData.integral),
// 红包个数
num: Number(formData.num || '1'),
// 发送类型
type: currentConversation?.value?.type,
title: text
}),
description: text,
extension: text
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID ||
currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload,
needReadReceipt: isEnabledMessageReadReceiptGlobal()
}
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value as IConversationModel,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_CUSTOM
}
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(
offlinePushInfoCreateParams
)
}
const isGroup = currentConversation?.value?.type === 'GROUP'
const data = {
password: e.join(''),
title: text,
packetType: isGroup ? 2 : 1,
receiverType: isGroup ? 2 : 1,
totalAmount: Number(formData.integral),
totalCount: Number(formData.num || '1')
}
try {
tixian.value.close()
await sendRedEnvelope(data)
TUIChatService.sendCustomMessage(
options as SendMessageParams,
sendMessageOptions
)
container?.value?.toggleDialogDisplay(false)
} catch (error) {
tixian.value.close()
}
} }
</script> </script>
@@ -71,6 +237,11 @@
@onDialogShow="onDialogShow" @onDialogShow="onDialogShow"
@onDialogClose="onDialogClose" @onDialogClose="onDialogClose"
> >
<uu-pwdModal
ref="tixian"
class="pwd-modal"
@success="pwdModalSubmit"
></uu-pwdModal>
<view class="red-envelope"> <view class="red-envelope">
<view class="top-title"> <view class="top-title">
<text class="title">发红包</text> <text class="title">发红包</text>
@@ -81,22 +252,59 @@
@click.stop="closeDialog" @click.stop="closeDialog"
></uni-icons> ></uni-icons>
</view> </view>
<!-- 输入框 --> <!-- 红包个数 -->
<view v-if="isGroup" class="group-box">
<view <view
:class="{ 'on-reminder': monitorPoints.valid }" :class="{ 'on-reminder': errorData.numShow }"
class="input-box" class="input-box"
> >
<view class="form-box">
<text>红包个数</text>
<view class="num-box">
<input
v-model="formData.num"
:placeholder-style="`font-family: PingFang SC, PingFang SC; font-weight: 500; color: ${
errorData.numShow ? '#f56c6c' : '#a9a9a9'
}; font-size: 32rpx; font-style: normal; text-transform: none;`"
confirm-type="done"
type="number"
placeholder="填写红包个数"
@input="numInput"
@confirm="onSubmit"
/>
<text></text>
</view>
</view>
<text v-if="errorData.numShow" class="error-text">
{{ monitorNum.message }}
</text>
</view>
<text class="num">本群共{{ memberCount }}</text>
</view>
<!-- 输入框 -->
<view
:class="{ 'on-reminder': errorData.integralShow }"
class="input-box"
>
<view class="form-box">
<text>积分</text> <text>积分</text>
<input <input
v-model="formData.integral" v-model="formData.integral"
:placeholder-style="placeholderStyle" :placeholder-style="`font-family: PingFang SC, PingFang SC; font-weight: 500; color: ${
errorData.integralShow ? '#f56c6c' : '#a9a9a9'
}; font-size: 32rpx; font-style: normal; text-transform: none;`"
confirm-type="done" confirm-type="done"
type="number" type="digit"
placeholder="0.00" placeholder="0.00"
@input="monitorInput"
@confirm="onSubmit" @confirm="onSubmit"
/> />
</view> </view>
<text v-if="errorData.integralShow" class="error-text">
{{ monitorPoints.message }}
</text>
</view>
<view class="input-box title-box"> <view class="input-box title-box">
<input <input
v-model="formData.title" v-model="formData.title"
@@ -115,15 +323,6 @@
<button class="btn" @click.stop="onSubmit">塞进红包</button> <button class="btn" @click.stop="onSubmit">塞进红包</button>
</view> </view>
</view> </view>
<!-- 提示框 -->
<uni-popup ref="message" type="message">
<uni-popup-message
type="error"
:message="monitorPoints.message"
:duration="2000"
></uni-popup-message>
</uni-popup>
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>

View File

@@ -5,7 +5,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 26rpx; margin-bottom: 30rpx;
.title { .title {
font-size: 32rpx; font-size: 32rpx;
color: #1c1c1c; color: #1c1c1c;
@@ -13,13 +13,19 @@
} }
.input-box { .input-box {
margin-bottom: 26rpx; margin-bottom: 60rpx;
background: #ffffff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx 34rpx; padding: 24rpx 34rpx;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.form-box {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
text { text {
font-size: 32rpx; font-size: 32rpx;
color: #1c1c1c; color: #1c1c1c;
@@ -30,6 +36,16 @@
color: #1c1c1c; color: #1c1c1c;
text-align: right; text-align: right;
} }
.num-box {
display: flex;
align-items: center;
justify-content: right;
text {
margin-left: 10rpx;
margin-bottom: 1rpx;
color: #1c1c1c;
}
}
} }
.title-box { .title-box {
@@ -73,11 +89,40 @@
// 警告提现 // 警告提现
.on-reminder { .on-reminder {
display: flex;
flex-direction: column;
align-items: flex-start;
text { text {
color: #f56c6c; color: #f56c6c !important;
} }
input { input {
color: #f56c6c; color: #f56c6c;
} }
.error-text {
margin-top: 10rpx;
font-size: 24rpx;
}
}
// 群红包个数
.group-box {
display: flex;
flex-direction: column;
.input-box {
margin-bottom: 10rpx;
}
.num {
margin-bottom: 30rpx;
margin-left: 34rpx;
font-size: 28rpx;
color: #5e5e5e;
}
}
}
.pwd-modal {
:deep(.modal) {
position: relative;
right: 20rpx;
} }
} }

View File

@@ -4,13 +4,13 @@
:class="[ :class="[
'toolbar-item-container', 'toolbar-item-container',
!isPC && 'toolbar-item-container-h5', !isPC && 'toolbar-item-container-h5',
isUniFrameWork && 'toolbar-item-container-uni', isUniFrameWork && 'toolbar-item-container-uni'
]" ]"
> >
<div <div
:class="[ :class="[
'toolbar-item-container-icon', 'toolbar-item-container-icon',
isUniFrameWork && 'toolbar-item-container-uni-icon', isUniFrameWork && 'toolbar-item-container-uni-icon'
]" ]"
@click="toggleToolbarItem" @click="toggleToolbarItem"
> >
@@ -34,7 +34,7 @@
'toolbar-item-container-dialog', 'toolbar-item-container-dialog',
isDark && 'toolbar-item-container-dialog-dark', isDark && 'toolbar-item-container-dialog-dark',
!isPC && 'toolbar-item-container-h5-dialog', !isPC && 'toolbar-item-container-h5-dialog',
isUniFrameWork && 'toolbar-item-container-uni-dialog', isUniFrameWork && 'toolbar-item-container-uni-dialog'
]" ]"
> >
<BottomPopup <BottomPopup
@@ -51,88 +51,95 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from '../../../../adapter-vue'; import { ref, watch } from '../../../../adapter-vue'
import { outsideClick } from '@tencentcloud/universal-api'; import { outsideClick } from '@tencentcloud/universal-api'
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue'
import BottomPopup from '../../../common/BottomPopup/index.vue'; import BottomPopup from '../../../common/BottomPopup/index.vue'
import { isPC, isUniFrameWork } from '../../../../utils/env'; import { isPC, isUniFrameWork } from '../../../../utils/env'
import TUIChatConfig from '../../config'; import TUIChatConfig from '../../config'
const props = defineProps({ const props = defineProps({
iconFile: { iconFile: {
type: String, type: String,
required: true, required: true
}, },
title: { title: {
type: String, type: String,
default: '', default: ''
}, },
needDialog: { needDialog: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
iconWidth: { iconWidth: {
type: String, type: String,
default: '20px', default: '20px'
}, },
iconHeight: { iconHeight: {
type: String, type: String,
default: '20px', default: '20px'
}, },
// Whether to display the bottom popup dialog on mobile devices // Whether to display the bottom popup dialog on mobile devices
// Invalid on PC // Invalid on PC
needBottomPopup: { needBottomPopup: {
type: Boolean, type: Boolean,
default: false, default: false
},
});
const emits = defineEmits(['onIconClick', 'onDialogClose', 'onDialogShow']);
const isDark = ref(TUIChatConfig.getTheme() === 'dark');
const showDialog = ref(false);
const toolbarItemRef = ref();
const dialogRef = ref();
watch(() => showDialog.value, (newVal) => {
if (!newVal) {
emits('onDialogClose', dialogRef);
} else {
emits('onDialogShow', dialogRef);
} }
}); })
const emits = defineEmits([
'onIconClick',
'onDialogClose',
'onDialogShow'
])
const isDark = ref(TUIChatConfig.getTheme() === 'dark')
const showDialog = ref(false)
const toolbarItemRef = ref()
const dialogRef = ref()
watch(
() => showDialog.value,
newVal => {
if (!newVal) {
emits('onDialogClose', dialogRef)
} else {
emits('onDialogShow', dialogRef)
}
}
)
const toggleToolbarItem = () => { const toggleToolbarItem = () => {
emits('onIconClick', dialogRef); emits('onIconClick', dialogRef)
if (isPC) { if (isPC) {
outsideClick.listen({ outsideClick.listen({
domRefs: toolbarItemRef.value, domRefs: toolbarItemRef.value,
handler: closeToolbarItem, handler: closeToolbarItem
}); })
} }
if (!props.needDialog) { if (!props.needDialog) {
return; return
}
toggleDialogDisplay(!showDialog.value)
} }
toggleDialogDisplay(!showDialog.value);
};
const closeToolbarItem = () => { const closeToolbarItem = () => {
showDialog.value = false; showDialog.value = false
}; }
const toggleDialogDisplay = (showStatus: boolean) => { const toggleDialogDisplay = (showStatus: boolean) => {
if (showDialog.value === showStatus) { if (showDialog.value === showStatus) {
return; return
}
showDialog.value = showStatus
} }
showDialog.value = showStatus;
};
const onPopupClose = () => { const onPopupClose = () => {
showDialog.value = false; showDialog.value = false
}; }
defineExpose({ defineExpose({
toggleDialogDisplay, toggleDialogDisplay
}); })
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -4,13 +4,20 @@
:class="{ :class="{
'input-quote-container': true, 'input-quote-container': true,
'input-quote-container-uni': isUniFrameWork, 'input-quote-container-uni': isUniFrameWork,
'input-quote-container-h5': isH5, 'input-quote-container-h5': isH5
}" }"
> >
<div class="input-quote-content"> <div class="input-quote-content">
<div class="max-one-line"> <div class="max-one-line">
{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }} {{ quoteMessage.nick || quoteMessage.from }}:
{{ quoteContentText }}
</div> </div>
<Icon
v-if="isRedEnvelope"
:file="unopenedEnvelope"
width="44rpx"
height="55rpx"
/>
<Icon <Icon
class="input-quote-close-icon" class="input-quote-close-icon"
:file="closeIcon" :file="closeIcon"
@@ -23,85 +30,124 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from '../../../../adapter-vue'; import {
ref,
computed,
onMounted,
onUnmounted
} from '../../../../adapter-vue'
import TUIChatEngine, { import TUIChatEngine, {
TUIStore, TUIStore,
StoreName, StoreName,
TUITranslateService, TUITranslateService,
IMessageModel, IMessageModel
} from '@tencentcloud/chat-uikit-engine-lite'; } from '@tencentcloud/chat-uikit-engine-lite'
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue'
import closeIcon from '../../../../assets/icon/icon-close.svg'; import closeIcon from '../../../../assets/icon/icon-close.svg'
import { isH5, isUniFrameWork } from '../../../../utils/env'; import { isH5, isUniFrameWork } from '../../../../utils/env'
import { transformTextWithKeysToEmojiNames } from '../../emoji-config'; import { transformTextWithKeysToEmojiNames } from '../../emoji-config'
import { InputDisplayType } from '../../../../interface'; import { InputDisplayType } from '../../../../interface'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../constant'
import unopenedEnvelope from '../../../../assets/icon/unopened-envelope.svg'
interface IProps { interface IProps {
displayType?: InputDisplayType; displayType?: InputDisplayType
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
displayType: 'editor', displayType: 'editor'
}); })
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES
const quoteMessage = ref<IMessageModel>(); const quoteMessage = ref<IMessageModel>()
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); })
}); })
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); })
}); })
/** 是否是红包 */
const isRedEnvelope = computed(() => {
if (quoteMessage.value?.payload?.data) {
const businessID = JSON?.parse(
quoteMessage.value?.payload?.data
)?.businessID
return businessID === CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE
}
return false
})
/** 红包内容 */
const quoteRedEnvelopeContentText = computed(() => {
if (isRedEnvelope.value) {
const data = JSON.parse(quoteMessage.value?.payload?.data)
return `${data.title}`
}
return ''
})
const quoteContentText = computed(() => { const quoteContentText = computed(() => {
let _quoteContentText; let _quoteContentText
if (isRedEnvelope.value) {
_quoteContentText = `${quoteRedEnvelopeContentText.value}`
} else {
switch (quoteMessage.value?.type) { switch (quoteMessage.value?.type) {
case TYPES.MSG_TEXT: case TYPES.MSG_TEXT:
_quoteContentText = transformTextWithKeysToEmojiNames(quoteMessage.value.payload?.text); _quoteContentText = transformTextWithKeysToEmojiNames(
break; quoteMessage.value.payload?.text
)
break
case TYPES.MSG_IMAGE: case TYPES.MSG_IMAGE:
_quoteContentText = TUITranslateService.t('TUIChat.图片'); _quoteContentText = TUITranslateService.t('TUIChat.图片')
break; break
case TYPES.MSG_AUDIO: case TYPES.MSG_AUDIO:
_quoteContentText = TUITranslateService.t('TUIChat.语音'); _quoteContentText = TUITranslateService.t('TUIChat.语音')
break; break
case TYPES.MSG_VIDEO: case TYPES.MSG_VIDEO:
_quoteContentText = TUITranslateService.t('TUIChat.视频'); _quoteContentText = TUITranslateService.t('TUIChat.视频')
break; break
case TYPES.MSG_FILE: case TYPES.MSG_FILE:
_quoteContentText = TUITranslateService.t('TUIChat.文件'); _quoteContentText = TUITranslateService.t('TUIChat.文件')
break; break
case TYPES.MSG_CUSTOM: case TYPES.MSG_CUSTOM:
_quoteContentText = TUITranslateService.t('TUIChat.自定义'); _quoteContentText = TUITranslateService.t('TUIChat.自定义')
break; break
case TYPES.MSG_FACE: case TYPES.MSG_FACE:
_quoteContentText = TUITranslateService.t('TUIChat.表情'); _quoteContentText = TUITranslateService.t('TUIChat.表情')
break; break
case TYPES.MSG_MERGER: case TYPES.MSG_MERGER:
_quoteContentText = TUITranslateService.t('TUIChat.聊天记录'); _quoteContentText = TUITranslateService.t('TUIChat.聊天记录')
break; break
default: default:
_quoteContentText = TUITranslateService.t('TUIChat.消息'); _quoteContentText = TUITranslateService.t('TUIChat.消息')
break; break
} }
return _quoteContentText; }
});
return _quoteContentText
})
function cancelQuote() { function cancelQuote() {
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: undefined, type: 'quote' }); TUIStore.update(StoreName.CHAT, 'quoteMessage', {
message: undefined,
type: 'quote'
})
} }
function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string }) { function onQuoteMessageUpdated(options?: {
message: IMessageModel
type: string
}) {
if (options?.message && options?.type === 'quote') { if (options?.message && options?.type === 'quote') {
quoteMessage.value = options.message; quoteMessage.value = options.message
} else { } else {
quoteMessage.value = undefined; quoteMessage.value = undefined
} }
} }
</script> </script>

View File

@@ -2,26 +2,23 @@
<div <div
:class="{ :class="{
'tui-chat': true, 'tui-chat': true,
'tui-chat-h5': isMobile, 'tui-chat-h5': isMobile
}" }"
@click="onMessageListBackgroundClick" @click="onMessageListBackgroundClick"
> >
<!-- <JoinGroupCard /> --> <!-- <JoinGroupCard /> -->
<div class="tui-chat-main"> <div class="tui-chat-main">
<div <div v-if="isOfficial" class="tui-chat-safe-tips">
v-if="isOfficial"
class="tui-chat-safe-tips"
>
<span> <span>
{{ {{
TUITranslateService.t( TUITranslateService.t(
"TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。" 'TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。'
) )
}} }}
</span> </span>
<a @click="openComplaintLink(Link.complaint)">{{ <a @click="openComplaintLink(Link.complaint)">
TUITranslateService.t("TUIChat.点此投诉") {{ TUITranslateService.t('TUIChat.点此投诉') }}
}}</a> </a>
</div> </div>
<MessageGroupApplication <MessageGroupApplication
v-if="isGroup" v-if="isGroup"
@@ -41,7 +38,7 @@
class="message-more" class="message-more"
@click="getHistoryMessageList" @click="getHistoryMessageList"
> >
{{ TUITranslateService.t("TUIChat.查看更多") }} {{ TUITranslateService.t('TUIChat.查看更多') }}
</p> </p>
<li <li
v-for="(item, index) in messageList" v-for="(item, index) in messageList"
@@ -53,12 +50,10 @@
:currTime="item.time" :currTime="item.time"
:prevTime="index > 0 ? messageList[index - 1].time : 0" :prevTime="index > 0 ? messageList[index - 1].time : 0"
/> />
<div <div class="message-item" @click="toggleID = ''">
class="message-item"
@click="toggleID = ''"
>
<MessageTip <MessageTip
v-if="item.type === TYPES.MSG_GRP_TIP || v-if="
item.type === TYPES.MSG_GRP_TIP ||
isCreateGroupCustomMessage(item) isCreateGroupCustomMessage(item)
" "
:content="item.getMessageContent()" :content="item.getMessageContent()"
@@ -67,10 +62,18 @@
v-else-if="!item.isRevoked && !isPluginMessage(item)" v-else-if="!item.isRevoked && !isPluginMessage(item)"
:id="`msg-bubble-${item.ID}`" :id="`msg-bubble-${item.ID}`"
class="message-bubble-container" class="message-bubble-container"
@longpress="handleToggleMessageItem($event, item, index, true)" @longpress="
@touchstart="handleH5LongPress($event, item, index, 'touchstart')" handleToggleMessageItem($event, item, index, true)
@touchend="handleH5LongPress($event, item, index, 'touchend')" "
@mouseover="handleH5LongPress($event, item, index, 'touchend')" @touchstart="
handleH5LongPress($event, item, index, 'touchstart')
"
@touchend="
handleH5LongPress($event, item, index, 'touchend')
"
@mouseover="
handleH5LongPress($event, item, index, 'touchend')
"
> >
<MessageBubble <MessageBubble
:messageItem="deepCopy(item)" :messageItem="deepCopy(item)"
@@ -78,7 +81,9 @@
:isAudioPlayed="audioPlayedMapping[item.ID]" :isAudioPlayed="audioPlayedMapping[item.ID]"
:blinkMessageIDList="blinkMessageIDList" :blinkMessageIDList="blinkMessageIDList"
:isMultipleSelectMode="isMultipleSelectMode" :isMultipleSelectMode="isMultipleSelectMode"
:multipleSelectedMessageIDList="multipleSelectedMessageIDList" :multipleSelectedMessageIDList="
multipleSelectedMessageIDList
"
@resendMessage="resendMessage(item)" @resendMessage="resendMessage(item)"
@blinkMessage="blinkMessage" @blinkMessage="blinkMessage"
@scrollTo="scrollTo" @scrollTo="scrollTo"
@@ -163,11 +168,13 @@
:class="{ :class="{
'message-tool': true, 'message-tool': true,
'message-tool-out': item.flow === 'out', 'message-tool-out': item.flow === 'out',
'message-tool-in': item.flow === 'in', 'message-tool-in': item.flow === 'in'
}" }"
:messageItem="item" :messageItem="item"
:isMultipleSelectMode="isMultipleSelectMode" :isMultipleSelectMode="isMultipleSelectMode"
@toggleMultipleSelectMode="() => emits('toggleMultipleSelectMode')" @toggleMultipleSelectMode="
() => emits('toggleMultipleSelectMode')
"
/> />
</div> </div>
</li> </li>
@@ -184,10 +191,10 @@
:center="true" :center="true"
:isHeaderShow="isPC" :isHeaderShow="isPC"
@submit="resendMessageConfirm()" @submit="resendMessageConfirm()"
@update:show="(e) => (reSendDialogShow = e)" @update:show="e => (reSendDialogShow = e)"
> >
<p class="delDialog-title"> <p class="delDialog-title">
{{ TUITranslateService.t("TUIChat.确认重发该消息?") }} {{ TUITranslateService.t('TUIChat.确认重发该消息?') }}
</p> </p>
</Dialog> </Dialog>
<!-- read receipt panel --> <!-- read receipt panel -->
@@ -220,209 +227,229 @@ import {
nextTick, nextTick,
onMounted, onMounted,
onUnmounted, onUnmounted,
getCurrentInstance, getCurrentInstance
} from '../../../adapter-vue'; } from '../../../adapter-vue'
import TUIChatEngine, { import TUIChatEngine, {
IMessageModel, IMessageModel,
TUIStore, TUIStore,
StoreName, StoreName,
TUITranslateService, TUITranslateService,
TUIChatService, TUIChatService
} from '@tencentcloud/chat-uikit-engine-lite'; } from '@tencentcloud/chat-uikit-engine-lite'
import { import {
setInstanceMapping, setInstanceMapping,
getBoundingClientRect, getBoundingClientRect,
getScrollInfo, getScrollInfo
} from '@tencentcloud/universal-api'; } from '@tencentcloud/universal-api'
// import { JoinGroupCard } from '@trtc/calls-uikit-wx'; // import { JoinGroupCard } from '@trtc/calls-uikit-wx';
import Link from './link'; import Link from './link'
import SimpleMessageList from './message-elements/simple-message-list/index.vue'; import SimpleMessageList from './message-elements/simple-message-list/index.vue'
import MessageGroupApplication from './message-group-application/index.vue'; import MessageGroupApplication from './message-group-application/index.vue'
import MessageText from './message-elements/message-text.vue'; import MessageText from './message-elements/message-text.vue'
import MessageImage from './message-elements/message-image.vue'; import MessageImage from './message-elements/message-image.vue'
import MessageAudio from './message-elements/message-audio.vue'; import MessageAudio from './message-elements/message-audio.vue'
import MessageRecord from './message-elements/message-record/index.vue'; import MessageRecord from './message-elements/message-record/index.vue'
import MessageFile from './message-elements/message-file.vue'; import MessageFile from './message-elements/message-file.vue'
import MessageFace from './message-elements/message-face.vue'; import MessageFace from './message-elements/message-face.vue'
import MessageCustom from './message-elements/message-custom.vue'; import MessageCustom from './message-elements/message-custom.vue'
import MessageTip from './message-elements/message-tip.vue'; import MessageTip from './message-elements/message-tip.vue'
import MessageBubble from './message-elements/message-bubble.vue'; import MessageBubble from './message-elements/message-bubble.vue'
import MessageLocation from './message-elements/message-location.vue'; import MessageLocation from './message-elements/message-location.vue'
import MessageTimestamp from './message-elements/message-timestamp.vue'; import MessageTimestamp from './message-elements/message-timestamp.vue'
import MessageVideo from './message-elements/message-video.vue'; import MessageVideo from './message-elements/message-video.vue'
import MessageTool from './message-tool/index.vue'; import MessageTool from './message-tool/index.vue'
import MessageRevoked from './message-tool/message-revoked.vue'; import MessageRevoked from './message-tool/message-revoked.vue'
import MessagePlugin from '../../../plugins/plugin-components/message-plugin.vue'; import MessagePlugin from '../../../plugins/plugin-components/message-plugin.vue'
import ReadReceiptPanel from './read-receipt-panel/index.vue'; import ReadReceiptPanel from './read-receipt-panel/index.vue'
import ScrollButton from './scroll-button/index.vue'; import ScrollButton from './scroll-button/index.vue'
import { isPluginMessage } from '../../../plugins/plugin-components/index'; import { isPluginMessage } from '../../../plugins/plugin-components/index'
import Dialog from '../../common/Dialog/index.vue'; import Dialog from '../../common/Dialog/index.vue'
import Drawer from '../../common/Drawer/index.vue'; import Drawer from '../../common/Drawer/index.vue'
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index'
import ProgressMessage from '../../common/ProgressMessage/index.vue'; import ProgressMessage from '../../common/ProgressMessage/index.vue'
import { isCreateGroupCustomMessage } from '../utils/utils'; import { isCreateGroupCustomMessage } from '../utils/utils'
import { isEnabledMessageReadReceiptGlobal, deepCopy } from '../utils/utils'; import {
import { throttle } from '../../../utils/lodash'; isEnabledMessageReadReceiptGlobal,
import { isPC, isH5, isMobile } from '../../../utils/env'; deepCopy
import chatStorage from '../utils/chatStorage'; } from '../utils/utils'
import { IAudioContext } from '../../../interface'; import { throttle } from '../../../utils/lodash'
import { isPC, isH5, isMobile } from '../../../utils/env'
import chatStorage from '../utils/chatStorage'
import { IAudioContext } from '../../../interface'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../constant'
interface IEmits { interface IEmits {
(e: 'closeInputToolBar'): void; (e: 'closeInputToolBar'): void
(e: 'handleEditor', message: IMessageModel, type: string): void; (e: 'handleEditor', message: IMessageModel, type: string): void
(key: 'toggleMultipleSelectMode'): void; (key: 'toggleMultipleSelectMode'): void
} }
interface IProps { interface IProps {
isGroup: boolean; isGroup: boolean
groupID: string; groupID: string
isNotInGroup: boolean; isNotInGroup: boolean
isMultipleSelectMode: boolean; isMultipleSelectMode: boolean
} }
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>()
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
isGroup: false, isGroup: false,
groupID: '', groupID: '',
isNotInGroup: false, isNotInGroup: false,
isMultipleSelectMode: false, isMultipleSelectMode: false
}); })
let selfAddValue = 0; let selfAddValue = 0
let observer: any = null; let observer: any = null
let groupType: string | undefined; let groupType: string | undefined
const sentReceiptMessageID = new Set<string>(); const sentReceiptMessageID = new Set<string>()
const isOfficial = TUIStore.getData(StoreName.APP, 'isOfficial'); const isOfficial = TUIStore.getData(StoreName.APP, 'isOfficial')
const thisInstance = getCurrentInstance()?.proxy || getCurrentInstance(); const thisInstance = getCurrentInstance()?.proxy || getCurrentInstance()
const messageList = ref<IMessageModel[]>(); const messageList = ref<IMessageModel[]>()
const multipleSelectedMessageIDList = ref<string[]>([]); const multipleSelectedMessageIDList = ref<string[]>([])
const isCompleted = ref(false); const isCompleted = ref(false)
const currentConversationID = ref(''); const currentConversationID = ref('')
const toggleID = ref(''); const toggleID = ref('')
const scrollTop = ref(5000); // The initial number of messages is 15, and the maximum message height is 300. const scrollTop = ref(5000) // The initial number of messages is 15, and the maximum message height is 300.
const TYPES = ref(TUIChatEngine.TYPES); const TYPES = ref(TUIChatEngine.TYPES)
const isLoadingMessage = ref(false); const isLoadingMessage = ref(false)
const isLongpressing = ref(false); const isLongpressing = ref(false)
const blinkMessageIDList = ref<string[]>([]); const blinkMessageIDList = ref<string[]>([])
const messageTarget = ref<IMessageModel>(); const messageTarget = ref<IMessageModel>()
const scrollButtonInstanceRef = ref<InstanceType<typeof ScrollButton>>(); const scrollButtonInstanceRef = ref<InstanceType<typeof ScrollButton>>()
const historyFirstMessageID = ref<string>(''); const historyFirstMessageID = ref<string>('')
const isShowSimpleMessageList = ref<boolean>(false); const isShowSimpleMessageList = ref<boolean>(false)
const simpleMessageListRenderMessageID = ref<string>(); const simpleMessageListRenderMessageID = ref<string>()
const audioPlayedMapping = ref<Record<string, boolean>>({}); const audioPlayedMapping = ref<Record<string, boolean>>({})
// audio control // audio control
const broadcastNewAudioSrc = ref<string>(''); const broadcastNewAudioSrc = ref<string>('')
const readStatusMessage = ref<IMessageModel>(); const readStatusMessage = ref<IMessageModel>()
const isShowReadUserStatusPanel = ref<boolean>(false); const isShowReadUserStatusPanel = ref<boolean>(false)
// Resend Message Dialog // Resend Message Dialog
const reSendDialogShow = ref(false); const reSendDialogShow = ref(false)
const resendMessageData = ref(); const resendMessageData = ref()
const scrollToBottom = () => { const scrollToBottom = () => {
scrollTop.value += 300; scrollTop.value += 300
// Solve the issue where swiping to the bottom for the first time after packaging Uniapp into an app has a delay, // Solve the issue where swiping to the bottom for the first time after packaging Uniapp into an app has a delay,
// which can be set to 300 ms. // which can be set to 300 ms.
const timer = setTimeout(() => { const timer = setTimeout(() => {
scrollTop.value += 1; scrollTop.value += 1
clearTimeout(timer); clearTimeout(timer)
}, 300); }, 300)
}; }
const onCurrentConversationIDUpdated = (conversationID: string) => { const onCurrentConversationIDUpdated = (conversationID: string) => {
currentConversationID.value = conversationID; currentConversationID.value = conversationID
if (isEnabledMessageReadReceiptGlobal()) { if (isEnabledMessageReadReceiptGlobal()) {
const { groupProfile } const { groupProfile } =
= TUIStore.getConversationModel(conversationID) || {}; TUIStore.getConversationModel(conversationID) || {}
groupType = groupProfile?.type; groupType = groupProfile?.type
} }
if (Object.keys(audioPlayedMapping.value).length > 0) { if (Object.keys(audioPlayedMapping.value).length > 0) {
// Synchronize storage about whether the audio has been played when converstaion switched // Synchronize storage about whether the audio has been played when converstaion switched
chatStorage.setChatStorage('audioPlayedMapping', audioPlayedMapping.value); chatStorage.setChatStorage(
'audioPlayedMapping',
audioPlayedMapping.value
)
}
} }
};
onMounted(() => { onMounted(() => {
// Retrieve the information about whether the audio has been played from localStorage // Retrieve the information about whether the audio has been played from localStorage
audioPlayedMapping.value = chatStorage.getChatStorage('audioPlayedMapping') || {}; audioPlayedMapping.value =
chatStorage.getChatStorage('audioPlayedMapping') || {}
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
messageSource: onMessageSourceUpdated, messageSource: onMessageSourceUpdated,
isCompleted: onChatCompletedUpdated, isCompleted: onChatCompletedUpdated
}); })
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); })
setInstanceMapping('messageList', thisInstance); setInstanceMapping('messageList', thisInstance)
uni.$on('scroll-to-bottom', scrollToLatestMessage); uni.$on('scroll-to-bottom', scrollToLatestMessage)
}); })
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
isCompleted: onChatCompletedUpdated, isCompleted: onChatCompletedUpdated
}); })
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); })
observer?.disconnect(); observer?.disconnect()
observer = null; observer = null
uni.$off('scroll-to-bottom'); uni.$off('scroll-to-bottom')
if (Object.keys(audioPlayedMapping.value).length > 0) { if (Object.keys(audioPlayedMapping.value).length > 0) {
// Synchronize storage about whether the audio has been played when the component is unmounted // Synchronize storage about whether the audio has been played when the component is unmounted
chatStorage.setChatStorage('audioPlayedMapping', audioPlayedMapping.value); chatStorage.setChatStorage(
'audioPlayedMapping',
audioPlayedMapping.value
)
} }
}); })
const handelScrollListScroll = throttle( const handelScrollListScroll = throttle(
function (e: Event) { function (e: Event) {
scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e); scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e)
}, },
500, 500,
{ leading: true }, { leading: true }
); )
function getGlobalAudioContext( function getGlobalAudioContext(
audioMap: Map<string, IAudioContext>, audioMap: Map<string, IAudioContext>,
options?: { newAudioSrc: string }, options?: { newAudioSrc: string }
) { ) {
if (options?.newAudioSrc) { if (options?.newAudioSrc) {
broadcastNewAudioSrc.value = options.newAudioSrc; broadcastNewAudioSrc.value = options.newAudioSrc
} }
} }
async function onMessageListUpdated(list: IMessageModel[]) { async function onMessageListUpdated(list: IMessageModel[]) {
observer?.disconnect(); observer?.disconnect()
messageList.value = list messageList.value = list
.filter(message => !message.isDeleted) .filter(message => !message.isDeleted)
.map((message) => { .map(message => {
message.vueForRenderKey = `${message.ID}`; message.vueForRenderKey = `${message.ID}`
return message; return message
}); })
const newLastMessage = messageList.value?.[messageList.value?.length - 1];
// messageList
const newLastMessage =
messageList.value?.[messageList.value?.length - 1]
if (messageTarget.value) { if (messageTarget.value) {
// scroll to target message // scroll to target message
scrollAndBlinkMessage(messageTarget.value); scrollAndBlinkMessage(messageTarget.value)
} else if (!isLoadingMessage.value && !(scrollButtonInstanceRef.value?.isScrollButtonVisible && newLastMessage?.flow === 'in')) { } else if (
!isLoadingMessage.value &&
!(
scrollButtonInstanceRef.value?.isScrollButtonVisible &&
newLastMessage?.flow === 'in'
)
) {
// scroll to bottom // scroll to bottom
nextTick(() => { nextTick(() => {
scrollToBottom(); scrollToBottom()
}); })
} }
if (isEnabledMessageReadReceiptGlobal()) { if (isEnabledMessageReadReceiptGlobal()) {
nextTick(() => bindIntersectionObserver()); nextTick(() => bindIntersectionObserver())
} }
} }
@@ -430,316 +457,341 @@ async function scrollToLatestMessage() {
try { try {
const { scrollHeight } = await getScrollInfo( const { scrollHeight } = await getScrollInfo(
'#messageScrollList', '#messageScrollList',
'messageList', 'messageList'
); )
if (scrollHeight) { if (scrollHeight) {
scrollTop.value === scrollHeight scrollTop.value === scrollHeight
? (scrollTop.value = scrollHeight + 1) ? (scrollTop.value = scrollHeight + 1)
: (scrollTop.value = scrollHeight); : (scrollTop.value = scrollHeight)
} else { } else {
scrollToBottom(); scrollToBottom()
} }
} catch (error) { } catch (error) {
scrollToBottom(); scrollToBottom()
} }
} }
async function onMessageSourceUpdated(message: IMessageModel) { async function onMessageSourceUpdated(message: IMessageModel) {
messageTarget.value = message; messageTarget.value = message
scrollAndBlinkMessage(messageTarget.value); scrollAndBlinkMessage(messageTarget.value)
} }
function scrollAndBlinkMessage(message: IMessageModel) { function scrollAndBlinkMessage(message: IMessageModel) {
if ( if (
messageList.value?.some( messageList.value?.some(
messageListItem => messageListItem?.ID === message?.ID, messageListItem => messageListItem?.ID === message?.ID
) )
) { ) {
nextTick(async () => { nextTick(async () => {
await scrollToTargetMessage(message); await scrollToTargetMessage(message)
await blinkMessage(message?.ID); await blinkMessage(message?.ID)
messageTarget.value = undefined; messageTarget.value = undefined
}); })
} }
} }
function onChatCompletedUpdated(flag: boolean) { function onChatCompletedUpdated(flag: boolean) {
isCompleted.value = flag; isCompleted.value = flag
} }
const getHistoryMessageList = () => { const getHistoryMessageList = () => {
isLoadingMessage.value = true; isLoadingMessage.value = true
const currentFirstMessageID = messageList.value?.[0]?.ID || ''; const currentFirstMessageID = messageList.value?.[0]?.ID || ''
TUIChatService.getMessageList().then(() => { TUIChatService.getMessageList().then(() => {
nextTick(() => { nextTick(() => {
historyFirstMessageID.value = currentFirstMessageID; historyFirstMessageID.value = currentFirstMessageID
const timer = setTimeout(() => { const timer = setTimeout(() => {
historyFirstMessageID.value = ''; historyFirstMessageID.value = ''
isLoadingMessage.value = false; isLoadingMessage.value = false
clearTimeout(timer); clearTimeout(timer)
}, 500); }, 500)
}); })
}); })
}; }
const openComplaintLink = () => { }; const openComplaintLink = () => {}
// toggle message // toggle message
const handleToggleMessageItem = ( const handleToggleMessageItem = (
e: any, e: any,
message: IMessageModel, message: IMessageModel,
index: number, index: number,
isLongpress = false, isLongpress = false
) => { ) => {
if (props.isMultipleSelectMode || props.isNotInGroup) { if (props.isMultipleSelectMode || props.isNotInGroup) {
return; return
} }
if (isLongpress) { if (isLongpress) {
isLongpressing.value = true; isLongpressing.value = true
}
toggleID.value = message.ID
} }
toggleID.value = message.ID;
};
// h5 long press // h5 long press
let timer: number; let timer: number
const handleH5LongPress = ( const handleH5LongPress = (
e: any, e: any,
message: IMessageModel, message: IMessageModel,
index: number, index: number,
type: string, type: string
) => { ) => {
if (props.isMultipleSelectMode || props.isNotInGroup) { if (props.isMultipleSelectMode || props.isNotInGroup) {
return; return
} }
if (!isH5) return; if (!isH5) return
function longPressHandler() { function longPressHandler() {
clearTimeout(timer); clearTimeout(timer)
handleToggleMessageItem(e, message, index, true); handleToggleMessageItem(e, message, index, true)
} }
function touchStartHandler() { function touchStartHandler() {
timer = setTimeout(longPressHandler, 500); timer = setTimeout(longPressHandler, 500)
} }
function touchEndHandler() { function touchEndHandler() {
clearTimeout(timer); clearTimeout(timer)
} }
switch (type) { switch (type) {
case 'touchstart': case 'touchstart':
touchStartHandler(); touchStartHandler()
break; break
case 'touchend': case 'touchend':
touchEndHandler(); touchEndHandler()
setTimeout(() => { setTimeout(() => {
isLongpressing.value = false; isLongpressing.value = false
}, 200); }, 200)
break; break
}
} }
};
// reedit message // reedit message
const handleEdit = (message: IMessageModel) => { const handleEdit = (message: IMessageModel) => {
emits('handleEditor', message, 'reedit'); emits('handleEditor', message, 'reedit')
}; }
const resendMessage = (message: IMessageModel) => { const resendMessage = (message: IMessageModel) => {
reSendDialogShow.value = true; reSendDialogShow.value = true
resendMessageData.value = message; resendMessageData.value = message
}; }
const handleImagePreview = (index: number) => { const handleImagePreview = (index: number) => {
if (!messageList.value) { if (!messageList.value) {
return; return
} }
const imageMessageIndex: number[] = []; const imageMessageIndex: number[] = []
const imageMessageList: IMessageModel[] = messageList.value.filter((item, index) => { const imageMessageList: IMessageModel[] = messageList.value.filter(
(item, index) => {
if ( if (
!item.isRevoked !item.isRevoked &&
&& !item.hasRiskContent !item.hasRiskContent &&
&& item.type === TYPES.value.MSG_IMAGE item.type === TYPES.value.MSG_IMAGE
) { ) {
imageMessageIndex.push(index); imageMessageIndex.push(index)
return true; return true
} }
return false; return false
}); }
)
uni.previewImage({ uni.previewImage({
current: imageMessageIndex.indexOf(index), current: imageMessageIndex.indexOf(index),
urls: imageMessageList.map(message => message.payload.imageInfoArray?.[2].url), urls: imageMessageList.map(
message => message.payload.imageInfoArray?.[2].url
),
// #ifdef APP-PLUS // #ifdef APP-PLUS
indicator: 'number', indicator: 'number'
// #endif // #endif
}); })
}; }
const resendMessageConfirm = () => { const resendMessageConfirm = () => {
reSendDialogShow.value = !reSendDialogShow.value; reSendDialogShow.value = !reSendDialogShow.value
const messageModel = resendMessageData.value; const messageModel = resendMessageData.value
messageModel.resendMessage(); messageModel.resendMessage()
}; }
function blinkMessage(messageID: string): Promise<void> { function blinkMessage(messageID: string): Promise<void> {
return new Promise((resolve) => { return new Promise(resolve => {
const index = blinkMessageIDList.value.indexOf(messageID); const index = blinkMessageIDList.value.indexOf(messageID)
if (index < 0) { if (index < 0) {
blinkMessageIDList.value.push(messageID); blinkMessageIDList.value.push(messageID)
const timer = setTimeout(() => { const timer = setTimeout(() => {
blinkMessageIDList.value.splice( blinkMessageIDList.value.splice(
blinkMessageIDList.value.indexOf(messageID), blinkMessageIDList.value.indexOf(messageID),
1, 1
); )
clearTimeout(timer); clearTimeout(timer)
resolve(); resolve()
}, 3000); }, 3000)
} }
}); })
} }
function scrollTo(scrollHeight: number) { function scrollTo(scrollHeight: number) {
scrollTop.value = scrollHeight; scrollTop.value = scrollHeight
} }
async function bindIntersectionObserver() { async function bindIntersectionObserver() {
if (!messageList.value || messageList.value.length === 0) { if (!messageList.value || messageList.value.length === 0) {
return; return
} }
if ( if (
groupType === TYPES.value.GRP_AVCHATROOM groupType === TYPES.value.GRP_AVCHATROOM ||
|| groupType === TYPES.value.GRP_COMMUNITY groupType === TYPES.value.GRP_COMMUNITY
) { ) {
// AVCHATROOM and COMMUNITY chats do not monitor read receipts for messages. // AVCHATROOM and COMMUNITY chats do not monitor read receipts for messages.
return; return
} }
observer?.disconnect(); observer?.disconnect()
observer = uni observer = uni
.createIntersectionObserver(thisInstance, { .createIntersectionObserver(thisInstance, {
threshold: [0.7], threshold: [0.7],
observeAll: true, observeAll: true
// In Uni-app, the `safetip` is also included, so a negative margin is needed to exclude it. // In Uni-app, the `safetip` is also included, so a negative margin is needed to exclude it.
}) })
.relativeTo('#messageScrollList', { top: -70 }); .relativeTo('#messageScrollList', { top: -70 })
observer?.observe('.message-li.in .message-bubble-container', (res: any) => { observer?.observe(
'.message-li.in .message-bubble-container',
(res: any) => {
if (sentReceiptMessageID.has(res.id)) { if (sentReceiptMessageID.has(res.id)) {
return; return
} }
const matchingMessage = messageList.value.find((message: IMessageModel) => { const matchingMessage = messageList.value.find(
return res.id.indexOf(message.ID) > -1; (message: IMessageModel) => {
}); return res.id.indexOf(message.ID) > -1
}
)
if ( if (
matchingMessage matchingMessage &&
&& matchingMessage.needReadReceipt matchingMessage.needReadReceipt &&
&& matchingMessage.flow === 'in' matchingMessage.flow === 'in' &&
&& !matchingMessage.readReceiptInfo?.isPeerRead !matchingMessage.readReceiptInfo?.isPeerRead
) { ) {
TUIChatService.sendMessageReadReceipt([matchingMessage]); TUIChatService.sendMessageReadReceipt([matchingMessage])
sentReceiptMessageID.add(res.id); sentReceiptMessageID.add(res.id)
} }
}); }
)
} }
function setReadReceiptPanelVisible(visible: boolean, message?: IMessageModel) { function setReadReceiptPanelVisible(
visible: boolean,
message?: IMessageModel
) {
if (visible && props.isNotInGroup) { if (visible && props.isNotInGroup) {
return; return
} }
if (!visible) { if (!visible) {
readStatusMessage.value = undefined; readStatusMessage.value = undefined
} else { } else {
readStatusMessage.value = message; readStatusMessage.value = message
} }
isShowReadUserStatusPanel.value = visible; isShowReadUserStatusPanel.value = visible
} }
async function scrollToTargetMessage(message: IMessageModel) { async function scrollToTargetMessage(message: IMessageModel) {
const targetMessageID = message.ID; const targetMessageID = message.ID
const isTargetMessageInScreen const isTargetMessageInScreen =
= messageList.value messageList.value &&
&& messageList.value.some(msg => msg.ID === targetMessageID); messageList.value.some(msg => msg.ID === targetMessageID)
if (targetMessageID && isTargetMessageInScreen) { if (targetMessageID && isTargetMessageInScreen) {
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
try { try {
const scrollViewRect = await getBoundingClientRect( const scrollViewRect = await getBoundingClientRect(
'#messageScrollList', '#messageScrollList',
'messageList', 'messageList'
); )
const originalMessageRect = await getBoundingClientRect( const originalMessageRect = await getBoundingClientRect(
'#tui-' + targetMessageID, '#tui-' + targetMessageID,
'messageList', 'messageList'
); )
const { scrollTop } = await getScrollInfo( const { scrollTop } = await getScrollInfo(
'#messageScrollList', '#messageScrollList',
'messageList', 'messageList'
); )
const finalScrollTop const finalScrollTop =
= originalMessageRect.top originalMessageRect.top +
+ scrollTop scrollTop -
- scrollViewRect.top scrollViewRect.top -
- (selfAddValue++ % 2); (selfAddValue++ % 2)
scrollTo(finalScrollTop); scrollTo(finalScrollTop)
clearTimeout(timer); clearTimeout(timer)
} catch (error) { } catch (error) {
// todo // todo
} }
}, 500); }, 500)
} else { } else {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.无法定位到原消息'), message: TUITranslateService.t('TUIChat.无法定位到原消息'),
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING
}); })
} }
} }
function onMessageListBackgroundClick() { function onMessageListBackgroundClick() {
emits('closeInputToolBar'); emits('closeInputToolBar')
} }
watch(() => props.isMultipleSelectMode, (newValue) => { watch(
() => props.isMultipleSelectMode,
newValue => {
if (!newValue) { if (!newValue) {
changeSelectMessageIDList({ changeSelectMessageIDList({
type: 'clearAll', type: 'clearAll',
messageID: '', messageID: ''
}); })
} }
}); }
)
function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove' | 'clearAll'; messageID: string }) { function changeSelectMessageIDList({
type,
messageID
}: {
type: 'add' | 'remove' | 'clearAll'
messageID: string
}) {
// TODO need to delete this // TODO need to delete this
if (type === 'clearAll') { if (type === 'clearAll') {
multipleSelectedMessageIDList.value = []; multipleSelectedMessageIDList.value = []
} else if (type === 'add' && !multipleSelectedMessageIDList.value.includes(messageID)) { } else if (
multipleSelectedMessageIDList.value.push(messageID); type === 'add' &&
!multipleSelectedMessageIDList.value.includes(messageID)
) {
multipleSelectedMessageIDList.value.push(messageID)
} else if (type === 'remove') { } else if (type === 'remove') {
multipleSelectedMessageIDList.value = multipleSelectedMessageIDList.value.filter(id => id !== messageID); multipleSelectedMessageIDList.value =
multipleSelectedMessageIDList.value.filter(id => id !== messageID)
} }
} }
function mergeForwardMessage() { function mergeForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', { TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: true, isMergeForward: true,
messageIDList: multipleSelectedMessageIDList.value, messageIDList: multipleSelectedMessageIDList.value
}); })
} }
function oneByOneForwardMessage() { function oneByOneForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', { TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: false, isMergeForward: false,
messageIDList: multipleSelectedMessageIDList.value, messageIDList: multipleSelectedMessageIDList.value
}); })
} }
function assignMessageIDInUniapp(messageID: string) { function assignMessageIDInUniapp(messageID: string) {
simpleMessageListRenderMessageID.value = messageID; simpleMessageListRenderMessageID.value = messageID
isShowSimpleMessageList.value = true; isShowSimpleMessageList.value = true
} }
function setAudioPlayed(messageID: string) { function setAudioPlayed(messageID: string) {
audioPlayedMapping.value[messageID] = true; audioPlayedMapping.value[messageID] = true
} }
defineExpose({ defineExpose({
oneByOneForwardMessage, oneByOneForwardMessage,
mergeForwardMessage, mergeForwardMessage,
scrollToLatestMessage, scrollToLatestMessage
}); })
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -72,6 +72,22 @@
</a> </a>
</div> </div>
</template> </template>
<template
v-else-if="
customData.businessID === CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE
"
>
<!-- 红包 -->
<view class="red-envelope">
<view class="top-title">
<Icon :file="unopenedEnvelope" width="78rpx" height="80rpx" />
<text class="title">
{{ customData.title }}
</text>
</view>
<text class="bottom-text">积分红包</text>
</view>
</template>
<template v-else> <template v-else>
<span v-html="content.custom" /> <span v-html="content.custom" />
</template> </template>
@@ -89,6 +105,8 @@
import { ICustomMessagePayload } from '../../../../interface' import { ICustomMessagePayload } from '../../../../interface'
import Icon from '../../../common/Icon.vue' import Icon from '../../../common/Icon.vue'
import star from '../../../../assets/icon/star-light.png' import star from '../../../../assets/icon/star-light.png'
import unopenedEnvelope from '../../../../assets/icon/unopened-envelope.svg'
interface Props { interface Props {
messageItem: IMessageModel messageItem: IMessageModel
content: any content: any
@@ -187,4 +205,32 @@
} }
} }
} }
.red-envelope {
display: flex;
flex-direction: column;
background: #f3901f;
border-radius: 16rpx;
padding: 20rpx;
.top-title {
display: flex;
align-items: center;
padding-bottom: 10rpx;
margin-bottom: 10rpx;
border-bottom: 2rpx solid #ffffff;
.title {
width: 40vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 32rpx;
font-weight: 500;
color: #ffffff;
}
}
.bottom-text {
font-size: 24rpx;
color: #ffffff;
}
}
</style> </style>

View File

@@ -1,8 +1,5 @@
<template> <template>
<div <div class="image-container" @click="handleImagePreview">
class="image-container"
@click="handleImagePreview"
>
<image <image
class="message-image" class="message-image"
mode="aspectFit" mode="aspectFit"
@@ -14,62 +11,62 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watchEffect, ref } from '../../../../adapter-vue'; import { watchEffect, ref } from '../../../../adapter-vue'
import type { IMessageModel } from '@tencentcloud/chat-uikit-engine-lite'; import type { IMessageModel } from '@tencentcloud/chat-uikit-engine-lite'
import type { IImageMessageContent } from '../../../../interface'; import type { IImageMessageContent } from '../../../../interface'
interface IProps { interface IProps {
content: IImageMessageContent; content: IImageMessageContent
messageItem: IMessageModel; messageItem: IMessageModel
} }
interface IEmit { interface IEmit {
(key: 'previewImage'): void; (key: 'previewImage'): void
} }
const emits = defineEmits<IEmit>(); const emits = defineEmits<IEmit>()
const props = withDefaults( const props = withDefaults(defineProps<IProps>(), {
defineProps<IProps>(),
{
content: () => ({}), content: () => ({}),
messageItem: () => ({} as IMessageModel), messageItem: () => ({} as IMessageModel)
}, })
);
const DEFAULT_MAX_SIZE = 155; const DEFAULT_MAX_SIZE = 155
const imageStyles = ref({ width: 'auto', height: 'auto' }); const imageStyles = ref({ width: 'auto', height: 'auto' })
const genImageStyles = (value: { width?: any; height?: any }) => { const genImageStyles = (value: { width?: any; height?: any }) => {
const { width, height } = value; const { width, height } = value
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return; return
} }
let imageWidth = 0; let imageWidth = 0
let imageHeight = 0; let imageHeight = 0
if (width >= height) { if (width >= height) {
imageWidth = DEFAULT_MAX_SIZE; imageWidth = DEFAULT_MAX_SIZE
imageHeight = (DEFAULT_MAX_SIZE * height) / width; imageHeight = (DEFAULT_MAX_SIZE * height) / width
} else { } else {
imageWidth = (DEFAULT_MAX_SIZE * width) / height; imageWidth = (DEFAULT_MAX_SIZE * width) / height
imageHeight = DEFAULT_MAX_SIZE; imageHeight = DEFAULT_MAX_SIZE
}
imageStyles.value.width = imageWidth + 'px'
imageStyles.value.height = imageHeight + 'px'
} }
imageStyles.value.width = imageWidth + 'px';
imageStyles.value.height = imageHeight + 'px';
};
watchEffect(() => { watchEffect(() => {
genImageStyles(props.content); genImageStyles(props.content)
}); })
const imageLoad = (event: Event) => { const imageLoad = (event: Event) => {
genImageStyles(event.detail); genImageStyles(event.detail)
}; }
const handleImagePreview = () => { const handleImagePreview = () => {
if (props.messageItem?.status === 'success' || props.messageItem.progress === 1) { if (
emits('previewImage'); props.messageItem?.status === 'success' ||
props.messageItem.progress === 1
) {
emits('previewImage')
}
} }
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,21 +1,20 @@
<template> <template>
<div> <div>
<div <div class="message-record-container" @click="openMergeDetail">
class="message-record-container" <div class="record-title">
@click="openMergeDetail"
>
<div
class="record-title"
>
{{ props.renderData.title }} {{ props.renderData.title }}
</div> </div>
<div class="record-abstract-container"> <div class="record-abstract-container">
<div <div
v-for="(item, index) in props.renderData.abstractList.slice(0, 7)" v-for="(item, index) in props.renderData.abstractList.slice(
0,
7
)"
:key="index" :key="index"
class="record-abstract-item" class="record-abstract-item"
> >
{{ transformTextWithKeysToEmojiNames(item) }} <!-- {{ transformTextWithKeysToEmojiNames(item) }} -->
{{ topName(item) }}
</div> </div>
</div> </div>
<div class="record-footer"> <div class="record-footer">
@@ -52,22 +51,26 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, withDefaults } from '../../../../../adapter-vue'; import { ref, computed, withDefaults } from '../../../../../adapter-vue'
import { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine-lite'; import {
import Overlay from '../../../../common/Overlay/index.vue'; TUITranslateService,
import Drawer from '../../../../common/Drawer/index.vue'; IMessageModel
import SimpleMessageList from '../simple-message-list/index.vue'; } from '@tencentcloud/chat-uikit-engine-lite'
import { isH5, isPC, isUniFrameWork } from '../../../../../utils/env'; import Overlay from '../../../../common/Overlay/index.vue'
import { transformTextWithKeysToEmojiNames } from '../../../emoji-config/index'; import Drawer from '../../../../common/Drawer/index.vue'
import { IMergeMessageContent } from '../../../../../interface'; import SimpleMessageList from '../simple-message-list/index.vue'
import { isH5, isPC, isUniFrameWork } from '../../../../../utils/env'
import { transformTextWithKeysToEmojiNames } from '../../../emoji-config/index'
import { IMergeMessageContent } from '../../../../../interface'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../../constant'
interface IEmits { interface IEmits {
(e: 'assignMessageIDInUniapp', messageID: string): void; (e: 'assignMessageIDInUniapp', messageID: string): void
} }
interface IProps { interface IProps {
// Core data for rendering message record card and message list // Core data for rendering message record card and message list
renderData: IMergeMessageContent; renderData: IMergeMessageContent
/** /**
* The MessageRecord component has two main functions: * The MessageRecord component has two main functions:
* 1. display message record cards primarily. * 1. display message record cards primarily.
@@ -76,31 +79,39 @@ interface IProps {
* it is only need renderData to render message record cards. * it is only need renderData to render message record cards.
* Therefore, 'messageItem' and 'disabled' is not a required prop. * Therefore, 'messageItem' and 'disabled' is not a required prop.
*/ */
disabled?: boolean; disabled?: boolean
messageItem?: IMessageModel; messageItem?: IMessageModel
} }
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>()
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
messageItem: () => ({}) as IMessageModel, messageItem: () => ({} as IMessageModel),
disabled: false, disabled: false
}); })
const isMessageListVisible = ref(false); const isMessageListVisible = ref(false)
// 把替[自定义消息]替换为积分红包
const topName = (str: string) => {
if (str.includes('[自定义消息]')) {
return str.replace('[自定义消息]', '[积分红包]')
} else {
return transformTextWithKeysToEmojiNames(str)
}
}
function openMergeDetail() { function openMergeDetail() {
if (props.disabled) { if (props.disabled) {
return; return
} }
if (!isUniFrameWork) { if (!isUniFrameWork) {
isMessageListVisible.value = true; isMessageListVisible.value = true
} else { } else {
emits('assignMessageIDInUniapp', props.messageItem.ID); emits('assignMessageIDInUniapp', props.messageItem.ID)
} }
} }
function closeMergeDetail() { function closeMergeDetail() {
isMessageListVisible.value = false; isMessageListVisible.value = false
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -2,20 +2,15 @@
<div <div
:class="{ :class="{
'simple-message-list-container': true, 'simple-message-list-container': true,
'simple-message-list-container-mobile': isMobile, 'simple-message-list-container-mobile': isMobile
}" }"
> >
<div class="header-container"> <div class="header-container">
<span <span class="back" @click="backPreviousLevel">
class="back" <Icon class="close-icon" :file="addIcon" :size="'18px'" />
@click="backPreviousLevel" <span v-if="isReturn">
> {{ TUITranslateService.t('TUIChat.返回') }}
<Icon </span>
class="close-icon"
:file="addIcon"
:size="'18px'"
/>
<span v-if="isReturn">{{ TUITranslateService.t('TUIChat.返回') }}</span>
<span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span> <span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span>
</span> </span>
@@ -23,9 +18,7 @@
{{ currentMergeMessageInfo.title }} {{ currentMergeMessageInfo.title }}
</span> </span>
</div> </div>
<div v-if="isDownloadOccurError"> <div v-if="isDownloadOccurError">Load Merge Message Error</div>
Load Merge Message Error
</div>
<div <div
v-else-if="isMergeMessageInfoLoaded" v-else-if="isMergeMessageInfoLoaded"
ref="simpleMessageListRef" ref="simpleMessageListRef"
@@ -35,7 +28,7 @@
v-for="item in currentMergeMessageInfo.messageList" v-for="item in currentMergeMessageInfo.messageList"
:key="item.ID" :key="item.ID"
:class="{ :class="{
'message-item': true, 'message-item': true
}" }"
> >
<MessageContainer <MessageContainer
@@ -50,20 +43,21 @@
class="message-text" class="message-text"
> >
<span <span
v-for="(textInfo, index) in parseTextToRenderArray(item.messageBody[0].payload['text'])" v-for="(textInfo, index) in parseTextToRenderArray(
item.messageBody[0].payload['text']
)"
:key="index" :key="index"
class="message-text-container" class="message-text-container"
> >
<span <span v-if="textInfo.type === 'text'" class="text">
v-if="textInfo.type === 'text'" {{ textInfo.content }}
class="text" </span>
>{{ textInfo.content }}</span>
<img <img
v-else v-else
class="simple-emoji" class="simple-emoji"
:src="textInfo.content" :src="textInfo.content"
alt="small-face" alt="small-face"
> />
</span> </span>
</div> </div>
<!-- image --> <!-- image -->
@@ -73,10 +67,12 @@
> >
<img <img
class="image" class="image"
:src="(item.messageBody[0].payload)['imageInfoArray'][2]['url']" :src="
item.messageBody[0].payload['imageInfoArray'][2]['url']
"
mode="widthFix" mode="widthFix"
alt="image" alt="image"
> />
</div> </div>
<!-- video --> <!-- video -->
<div <div
@@ -85,29 +81,30 @@
> >
<div <div
v-if="isUniFrameWork" v-if="isUniFrameWork"
@click="previewVideoInUniapp((item.messageBody[0].payload)['remoteVideoUrl'])" @click="
previewVideoInUniapp(
item.messageBody[0].payload['remoteVideoUrl']
)
"
> >
<image <image
class="image" class="image"
:src="(item.messageBody[0].payload)['thumbUrl']" :src="item.messageBody[0].payload['thumbUrl']"
mode="widthFix" mode="widthFix"
alt="image" alt="image"
/> />
<Icon <Icon class="video-play-icon" :file="playIcon" />
class="video-play-icon"
:file="playIcon"
/>
</div> </div>
<video <video
v-else v-else
class="video" class="video"
controls controls
:poster="(item.messageBody[0].payload)['thumbUrl']" :poster="item.messageBody[0].payload['thumbUrl']"
> >
<source <source
:src="(item.messageBody[0].payload)['remoteVideoUrl']" :src="item.messageBody[0].payload['remoteVideoUrl']"
type="video/mp4" type="video/mp4"
> />
</video> </video>
</div> </div>
<!-- audio --> <!-- audio -->
@@ -115,7 +112,7 @@
v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO" v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO"
class="message-audio" class="message-audio"
> >
<span>{{ TUITranslateService.t("TUIChat.语音") }}&nbsp;</span> <span>{{ TUITranslateService.t('TUIChat.语音') }}&nbsp;</span>
<span>{{ item.messageBody[0].payload.second }}s</span> <span>{{ item.messageBody[0].payload.second }}s</span>
</div> </div>
<!-- big face --> <!-- big face -->
@@ -127,7 +124,7 @@
class="image" class="image"
:src="resolveBigFaceUrl(item.messageBody[0].payload.data)" :src="resolveBigFaceUrl(item.messageBody[0].payload.data)"
alt="face" alt="face"
> />
</div> </div>
<!-- file --> <!-- file -->
<div <div
@@ -153,6 +150,10 @@
:renderData="item.messageBody[0].payload" :renderData="item.messageBody[0].payload"
/> />
</div> </div>
<!-- 红包 -->
<div v-else-if="isRedEnvelope(item)">
{{ redEnvelopeText(item) }}
</div>
<!-- custom --> <!-- custom -->
<div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM"> <div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM">
{{ TUITranslateService.t('TUIChat.[自定义消息]') }} {{ TUITranslateService.t('TUIChat.[自定义消息]') }}
@@ -164,20 +165,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from '../../../../../adapter-vue'; import { computed, ref, watch } from '../../../../../adapter-vue'
import TUIChatEngine, { import TUIChatEngine, {
TUIStore, TUIStore,
TUIChatService, TUIChatService,
TUITranslateService, TUITranslateService
} from '@tencentcloud/chat-uikit-engine-lite'; } from '@tencentcloud/chat-uikit-engine-lite'
import addIcon from '../../../../../assets/icon/back.svg'; import addIcon from '../../../../../assets/icon/back.svg'
import playIcon from '../../../../../assets/icon/video-play.png'; import playIcon from '../../../../../assets/icon/video-play.png'
import Icon from '../../../../common/Icon.vue'; import Icon from '../../../../common/Icon.vue'
import MessageContainer from './message-container.vue'; import MessageContainer from './message-container.vue'
import MessageRecord from '../message-record/index.vue'; import MessageRecord from '../message-record/index.vue'
import { parseTextToRenderArray, DEFAULT_BIG_EMOJI_URL, CUSTOM_BIG_EMOJI_URL } from '../../../emoji-config/index'; import {
import { isMobile, isUniFrameWork } from '../../../../../utils/env'; parseTextToRenderArray,
import { IMergeMessageContent } from '../../../../../interface'; DEFAULT_BIG_EMOJI_URL,
CUSTOM_BIG_EMOJI_URL
} from '../../../emoji-config/index'
import { isMobile, isUniFrameWork } from '../../../../../utils/env'
import { IMergeMessageContent } from '../../../../../interface'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../../constant'
interface IProps { interface IProps {
/** /**
@@ -186,107 +192,142 @@ interface IProps {
* need to download message from sdk by constructed message * need to download message from sdk by constructed message
* and use downloaded message object to render nested simple-message-list * and use downloaded message object to render nested simple-message-list
*/ */
messageID?: string; messageID?: string
isMounted?: boolean; isMounted?: boolean
} }
interface IEmits { interface IEmits {
(e: 'closeOverlay'): void; (e: 'closeOverlay'): void
} }
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>()
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
messageID: '', messageID: '',
isMounted: false, isMounted: false
}); })
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES
const isDownloadOccurError = ref(false); const isDownloadOccurError = ref(false)
const messageListStack = ref<IMergeMessageContent[]>([]); const messageListStack = ref<IMergeMessageContent[]>([])
const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({ const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({
title: '', title: '',
messageList: [], messageList: []
}); })
const simpleMessageListRef = ref<HTMLElement>(); const simpleMessageListRef = ref<HTMLElement>()
watch(() => messageListStack.value.length, async (newValue) => { watch(
isDownloadOccurError.value = false; () => messageListStack.value.length,
async newValue => {
isDownloadOccurError.value = false
if (newValue < 1) { if (newValue < 1) {
return; return
} }
const stackTopMessageInfo = messageListStack.value[messageListStack.value.length - 1]; const stackTopMessageInfo =
if (stackTopMessageInfo.downloadKey && stackTopMessageInfo.messageList.length === 0) { messageListStack.value[messageListStack.value.length - 1]
if (
stackTopMessageInfo.downloadKey &&
stackTopMessageInfo.messageList.length === 0
) {
try { try {
const res = await TUIChatService.downloadMergedMessages({ const res = await TUIChatService.downloadMergedMessages({
payload: stackTopMessageInfo, payload: stackTopMessageInfo,
type: TUIChatEngine.TYPES.MSG_MERGER, type: TUIChatEngine.TYPES.MSG_MERGER
} as any); } as any)
// if download complete message, cover the original message in stack top // if download complete message, cover the original message in stack top
messageListStack.value[messageListStack.value.length - 1] = res.payload; messageListStack.value[messageListStack.value.length - 1] =
res.payload
} catch (error) { } catch (error) {
isDownloadOccurError.value = true; isDownloadOccurError.value = true
} }
} }
currentMergeMessageInfo.value = messageListStack.value[messageListStack.value.length - 1]; 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 // For compatibility with uniapp, use watch to implement onMounted
if (newValue) { if (newValue) {
if (!props.messageID) { if (!props.messageID) {
throw new Error('messageID is required when first render of simple-message-list.'); throw new Error(
'messageID is required when first render of simple-message-list.'
)
} }
const sdkMessagePayload = TUIStore.getMessageModel(props.messageID).getMessage().payload; const sdkMessagePayload = TUIStore.getMessageModel(
messageListStack.value = [sdkMessagePayload]; props.messageID
).getMessage().payload
messageListStack.value = [sdkMessagePayload]
} else { } else {
messageListStack.value = []; messageListStack.value = []
} }
}, { },
immediate: true, {
}); immediate: true
}
)
const isReturn = computed(() => { const isReturn = computed(() => {
return messageListStack.value.length > 1; return messageListStack.value.length > 1
}); })
const isMergeMessageInfoLoaded = computed(() => { const isMergeMessageInfoLoaded = computed(() => {
return currentMergeMessageInfo.value?.messageList ? currentMergeMessageInfo.value.messageList.length > 0 : false; return currentMergeMessageInfo.value?.messageList
}); ? currentMergeMessageInfo.value.messageList.length > 0
: false
})
function entryNextLevel(e, sdkMessage: any) { function entryNextLevel(e, sdkMessage: any) {
messageListStack.value.push(sdkMessage.messageBody[0].payload); messageListStack.value.push(sdkMessage.messageBody[0].payload)
e.stopPropagation(); e.stopPropagation()
} }
function backPreviousLevel() { function backPreviousLevel() {
messageListStack.value.pop(); messageListStack.value.pop()
if (messageListStack.value.length < 1) { if (messageListStack.value.length < 1) {
emits('closeOverlay'); emits('closeOverlay')
} }
} }
function previewVideoInUniapp(url: string) { function previewVideoInUniapp(url: string) {
if (isUniFrameWork) { if (isUniFrameWork) {
const encodedUrl = encodeURIComponent(url); const encodedUrl = encodeURIComponent(url)
uni.navigateTo({ uni.navigateTo({
url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`, url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`
}); })
} }
} }
function resolveBigFaceUrl(bigFaceKey: string): string { function resolveBigFaceUrl(bigFaceKey: string): string {
let url = ''; let url = ''
if (bigFaceKey.indexOf('@custom') > -1) { if (bigFaceKey.indexOf('@custom') > -1) {
url = CUSTOM_BIG_EMOJI_URL + bigFaceKey; url = CUSTOM_BIG_EMOJI_URL + bigFaceKey
} else { } else {
url = DEFAULT_BIG_EMOJI_URL + bigFaceKey; url = DEFAULT_BIG_EMOJI_URL + bigFaceKey
if (url.indexOf('@2x') === -1) { if (url.indexOf('@2x') === -1) {
url += '@2x.png'; url += '@2x.png'
} else { } else {
url += '.png'; url += '.png'
} }
} }
return url; return url
}
/** 是否红包 */
const isRedEnvelope = (item: any) => {
if (item.messageBody[0]?.payload?.data) {
const businessID = JSON?.parse(
item.messageBody[0]?.payload?.data
)?.businessID
return businessID === CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE
}
return false
}
/** 红包文案 */
const redEnvelopeText = (item: any) => {
const payload = JSON.parse(item.messageBody[0]?.payload?.data)
return `[积分红包] ${payload.title}`
} }
</script> </script>
@@ -344,7 +385,6 @@ function resolveBigFaceUrl(bigFaceKey: string): string {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.message-list { .message-list {

View File

@@ -250,7 +250,23 @@ export const refuseFriendApplication = (userID: string) => {
// Dismiss group // Dismiss group
export const dismissGroup = (groupID: string) => { export const dismissGroup = (groupID: string) => {
deleteImGroup(groupID) // deleteImGroup(groupID)
// .then(() => {
// Toast({
// message: TUITranslateService.t('TUIContact.解散群聊成功'),
// type: TOAST_TYPE.SUCCESS
// })
// TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch()
// switchTab('/TUIKit/components/TUIConversation/index')
// })
// .catch((error: any) => {
// console.warn('dismiss group failed:', error)
// Toast({
// message: TUITranslateService.t('TUIContact.解散群聊失败'),
// type: TOAST_TYPE.ERROR
// })
// })
TUIGroupService.dismissGroup(groupID)
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.解散群聊成功'), message: TUITranslateService.t('TUIContact.解散群聊成功'),
@@ -266,33 +282,34 @@ export const dismissGroup = (groupID: string) => {
type: TOAST_TYPE.ERROR type: TOAST_TYPE.ERROR
}) })
}) })
// TUIGroupService.dismissGroup(groupID)
// .then(() => {
// Toast({
// message: TUITranslateService.t('TUIContact.解散群聊成功'),
// type: TOAST_TYPE.SUCCESS,
// });
// TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch();
// })
// .catch((error: any) => {
// console.warn('dismiss group failed:', error);
// Toast({
// message: TUITranslateService.t('TUIContact.解散群聊失败'),
// type: TOAST_TYPE.ERROR,
// });
// });
} }
// Quit group // Quit group
export const quitGroup = (groupID: string) => { export const quitGroup = (groupID: string) => {
console.log('222') // console.log('222')
quitImGroup(groupID) // quitImGroup(groupID)
// .then(() => {
// Toast({
// message: TUITranslateService.t('TUIContact.退出群组成功'),
// type: TOAST_TYPE.SUCCESS
// })
// switchTab('/TUIKit/components/TUIConversation/index')
// })
// .catch((error: any) => {
// console.warn('quit group failed:', error)
// Toast({
// message: TUITranslateService.t('TUIContact.退出群组失败'),
// type: TOAST_TYPE.ERROR
// })
// })
TUIGroupService.quitGroup(groupID)
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.退出群组成功'), message: TUITranslateService.t('TUIContact.退出群组成功'),
type: TOAST_TYPE.SUCCESS type: TOAST_TYPE.SUCCESS
}) })
switchTab('/TUIKit/components/TUIConversation/index') switchTab('/TUIKit/components/TUIConversation/index')
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -302,21 +319,6 @@ export const quitGroup = (groupID: string) => {
type: TOAST_TYPE.ERROR type: TOAST_TYPE.ERROR
}) })
}) })
// TUIGroupService.quitGroup(groupID)
// .then(() => {
// Toast({
// message: TUITranslateService.t('TUIContact.退出群组成功'),
// type: TOAST_TYPE.SUCCESS,
// });
// })
// .catch((error: any) => {
// console.warn('quit group failed:', error);
// Toast({
// message: TUITranslateService.t('TUIContact.退出群组失败'),
// type: TOAST_TYPE.ERROR,
// });
// });
} }
// Join group // Join group

View File

@@ -1,8 +1,5 @@
<template> <template>
<div <div ref="conversationListInnerDomRef" class="tui-conversation-list">
ref="conversationListInnerDomRef"
class="tui-conversation-list"
>
<ActionsMenu <ActionsMenu
v-if="isShowOverlay" v-if="isShowOverlay"
:selectedConversation="currentConversation" :selectedConversation="currentConversation"
@@ -16,7 +13,7 @@
:key="index" :key="index"
:class="[ :class="[
'tui-conversation-content', 'tui-conversation-content',
isMobile && 'tui-conversation-content-h5 disable-select', isMobile && 'tui-conversation-content-h5 disable-select'
]" ]"
> >
<div <div
@@ -25,11 +22,15 @@
'tui-conversation-item', 'tui-conversation-item',
currentConversationID === conversation.conversationID && currentConversationID === conversation.conversationID &&
'tui-conversation-item-selected', 'tui-conversation-item-selected',
conversation.isPinned && 'tui-conversation-item-pinned', conversation.isPinned && 'tui-conversation-item-pinned'
]" ]"
@click="enterConversationChat(conversation.conversationID)" @click="enterConversationChat(conversation.conversationID)"
@longpress="showConversationActionMenu($event, conversation, index)" @longpress="
@contextmenu="showConversationActionMenu($event, conversation, index, true)" showConversationActionMenu($event, conversation, index)
"
@contextmenu="
showConversationActionMenu($event, conversation, index, true)
"
> >
<aside class="left"> <aside class="left">
<Avatar <Avatar
@@ -38,7 +39,9 @@
size="40px" size="40px"
/> />
<div <div
v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)" v-if="
userOnlineStatusMap && isShowUserOnlineStatus(conversation)
"
:class="[ :class="[
'online-status', 'online-status',
Object.keys(userOnlineStatusMap).length > 0 && Object.keys(userOnlineStatusMap).length > 0 &&
@@ -48,7 +51,7 @@
userOnlineStatusMap[conversation.userProfile.userID] userOnlineStatusMap[conversation.userProfile.userID]
.statusType === 1 .statusType === 1
? 'online-status-online' ? 'online-status-online'
: 'online-status-offline', : 'online-status-offline'
]" ]"
/> />
<span <span
@@ -56,7 +59,9 @@
class="num" class="num"
> >
{{ {{
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount conversation.unreadCount > 99
? '99+'
: conversation.unreadCount
}} }}
</span> </span>
<span <span
@@ -69,11 +74,21 @@
<label class="content-header-label"> <label class="content-header-label">
<p class="name">{{ conversation.getShowName() }}</p> <p class="name">{{ conversation.getShowName() }}</p>
</label> </label>
<div class="middle-box"> <div v-if="isRedEnvelope(conversation)" class="middle-box">
<div class="middle-box-content">
{{ redEnvelopeText(conversation) }}
</div>
</div>
<div v-else class="middle-box">
<span <span
v-if="conversation.draftText && conversation.conversationID !== currentConversationID" v-if="
conversation.draftText &&
conversation.conversationID !== currentConversationID
"
class="middle-box-draft" class="middle-box-draft"
>{{ TUITranslateService.t('TUIChat.[草稿]') }}</span> >
{{ TUITranslateService.t('TUIChat.[草稿]') }}
</span>
<span <span
v-else-if=" v-else-if="
conversation.type === 'GROUP' && conversation.type === 'GROUP' &&
@@ -81,14 +96,18 @@
conversation.groupAtInfoList.length > 0 conversation.groupAtInfoList.length > 0
" "
class="middle-box-at" class="middle-box-at"
>{{ conversation.getGroupAtInfo() }}</span> >
{{ conversation.getGroupAtInfo() }}
</span>
<div class="middle-box-content"> <div class="middle-box-content">
{{ conversation.getLastMessage("text") }} {{ conversation.getLastMessage('text') }}
</div> </div>
</div> </div>
</div> </div>
<div class="content-footer"> <div class="content-footer">
<span class="time">{{ conversation.getLastMessage("time") }}</span> <span class="time">
{{ conversation.getLastMessage('time') }}
</span>
<Icon <Icon
v-if="conversation.isMuted" v-if="conversation.isMuted"
:file="muteIcon" :file="muteIcon"
@@ -103,199 +122,251 @@
<script lang="ts" setup> <script lang="ts" setup>
interface IUserStatus { interface IUserStatus {
statusType: number; statusType: number
customStatus: string; customStatus: string
} }
interface IUserStatusMap { interface IUserStatusMap {
[userID: string]: IUserStatus; [userID: string]: IUserStatus
} }
import { ref, onMounted, onUnmounted } from '../../../adapter-vue'; import { ref, onMounted, onUnmounted } from '../../../adapter-vue'
import TUIChatEngine, { import TUIChatEngine, {
TUIStore, TUIStore,
StoreName, StoreName,
TUIConversationService, TUIConversationService,
TUITranslateService, TUITranslateService,
IConversationModel, IConversationModel
} from '@tencentcloud/chat-uikit-engine-lite'; } from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal, isIOS, addLongPressListener } from '@tencentcloud/universal-api'; import {
import Icon from '../../common/Icon.vue'; TUIGlobal,
import Avatar from '../../common/Avatar/index.vue'; isIOS,
import ActionsMenu from '../actions-menu/index.vue'; addLongPressListener
import muteIcon from '../../../assets/icon/mute.svg'; } from '@tencentcloud/universal-api'
import { isPC, isH5, isUniFrameWork, isMobile } from '../../../utils/env'; import Icon from '../../common/Icon.vue'
import Avatar from '../../common/Avatar/index.vue'
import ActionsMenu from '../actions-menu/index.vue'
import muteIcon from '../../../assets/icon/mute.svg'
import {
isPC,
isH5,
isUniFrameWork,
isMobile
} from '../../../utils/env'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../constant'
const emits = defineEmits(['handleSwitchConversation', 'getPassingRef']); const emits = defineEmits(['handleSwitchConversation', 'getPassingRef'])
const currentConversation = ref<IConversationModel>(); const currentConversation = ref<IConversationModel>()
const currentConversationID = ref<string>(); const currentConversationID = ref<string>()
const currentConversationDomRect = ref<DOMRect>(); const currentConversationDomRect = ref<DOMRect>()
const isShowOverlay = ref<boolean>(false); const isShowOverlay = ref<boolean>(false)
const conversationList = ref<IConversationModel[]>([]); const conversationList = ref<IConversationModel[]>([])
const conversationListDomRef = ref<HTMLElement | undefined>(); const conversationListDomRef = ref<HTMLElement | undefined>()
const conversationListInnerDomRef = ref<HTMLElement | undefined>(); const conversationListInnerDomRef = ref<HTMLElement | undefined>()
const actionsMenuPosition = ref<{ const actionsMenuPosition = ref<{
top: number; top: number
left: number | undefined; left: number | undefined
conversationHeight: number | undefined; conversationHeight: number | undefined
}>({ }>({
top: 0, top: 0,
left: undefined, left: undefined,
conversationHeight: undefined, conversationHeight: undefined
}); })
const displayOnlineStatus = ref(false); const displayOnlineStatus = ref(false)
const userOnlineStatusMap = ref<IUserStatusMap>(); const userOnlineStatusMap = ref<IUserStatusMap>()
let lastestOpenActionsMenuTime: number | null = null; let lastestOpenActionsMenuTime: number | null = null
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated, conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); })
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); })
if (!isUniFrameWork && isIOS && !isPC) { if (!isUniFrameWork && isIOS && !isPC) {
addLongPressHandler(); addLongPressHandler()
} }
}); })
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated, conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); })
TUIStore.unwatch(StoreName.USER, { TUIStore.unwatch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); })
}); })
const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => { const isShowUserOnlineStatus = (
conversation: IConversationModel
): boolean => {
return ( return (
displayOnlineStatus.value displayOnlineStatus.value &&
&& conversation.type === TUIChatEngine.TYPES.CONV_C2C conversation.type === TUIChatEngine.TYPES.CONV_C2C
); )
}; }
const showConversationActionMenu = ( const showConversationActionMenu = (
event: Event, event: Event,
conversation: IConversationModel, conversation: IConversationModel,
index: number, index: number,
isContextMenuEvent?: boolean, isContextMenuEvent?: boolean
) => { ) => {
if (isContextMenuEvent) { if (isContextMenuEvent) {
event.preventDefault(); event.preventDefault()
if (isUniFrameWork) { if (isUniFrameWork) {
return; return
} }
} }
currentConversation.value = conversation; currentConversation.value = conversation
lastestOpenActionsMenuTime = Date.now(); lastestOpenActionsMenuTime = Date.now()
getActionsMenuPosition(event, index); getActionsMenuPosition(event, index)
}; }
const closeConversationActionMenu = () => { const closeConversationActionMenu = () => {
// Prevent continuous triggering of overlay tap events // Prevent continuous triggering of overlay tap events
if ( if (
lastestOpenActionsMenuTime lastestOpenActionsMenuTime &&
&& Date.now() - lastestOpenActionsMenuTime > 300 Date.now() - lastestOpenActionsMenuTime > 300
) { ) {
currentConversation.value = undefined; currentConversation.value = undefined
isShowOverlay.value = false; isShowOverlay.value = false
}
} }
};
const getActionsMenuPosition = (event: Event, index: number) => { const getActionsMenuPosition = (event: Event, index: number) => {
if (isUniFrameWork) { if (isUniFrameWork) {
if (typeof conversationListDomRef.value === 'undefined') { if (typeof conversationListDomRef.value === 'undefined') {
emits('getPassingRef', conversationListDomRef); emits('getPassingRef', conversationListDomRef)
} }
const query = TUIGlobal?.createSelectorQuery().in(conversationListDomRef.value); const query = TUIGlobal?.createSelectorQuery().in(
query.select(`#convlistitem-${index}`).boundingClientRect((data) => { conversationListDomRef.value
)
query
.select(`#convlistitem-${index}`)
.boundingClientRect(data => {
if (data) { if (data) {
actionsMenuPosition.value = { 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. // 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 - 44, top: data.bottom - 44,
// @ts-expect-error in uniapp event has touches property // @ts-expect-error in uniapp event has touches property
left: event.touches[0].pageX, left: event.touches[0].pageX,
conversationHeight: data.height, conversationHeight: data.height
};
isShowOverlay.value = true;
} }
}).exec(); isShowOverlay.value = true
}
})
.exec()
} else { } else {
const rect = ((event.currentTarget || event.target) as HTMLElement)?.getBoundingClientRect() || {}; const rect =
(
(event.currentTarget || event.target) as HTMLElement
)?.getBoundingClientRect() || {}
if (rect) { if (rect) {
actionsMenuPosition.value = { actionsMenuPosition.value = {
top: rect.bottom, top: rect.bottom,
left: isPC ? (event as MouseEvent).clientX : undefined, left: isPC ? (event as MouseEvent).clientX : undefined,
conversationHeight: rect.height, conversationHeight: rect.height
}; }
}
isShowOverlay.value = true
} }
isShowOverlay.value = true;
} }
};
const enterConversationChat = (conversationID: string) => { const enterConversationChat = (conversationID: string) => {
emits('handleSwitchConversation', conversationID); emits('handleSwitchConversation', conversationID)
TUIConversationService.switchConversation(conversationID); TUIConversationService.switchConversation(conversationID)
}; }
function addLongPressHandler() { function addLongPressHandler() {
if (!conversationListInnerDomRef.value) { if (!conversationListInnerDomRef.value) {
return; return
} }
addLongPressListener({ addLongPressListener({
element: conversationListInnerDomRef.value, element: conversationListInnerDomRef.value,
onLongPress: (event, target) => { onLongPress: (event, target) => {
const index = (Array.from(conversationListInnerDomRef.value!.children) as HTMLElement[]).indexOf(target!); const index = (
showConversationActionMenu(event, conversationList.value[index], index); Array.from(
conversationListInnerDomRef.value!.children
) as HTMLElement[]
).indexOf(target!)
showConversationActionMenu(
event,
conversationList.value[index],
index
)
}, },
options: { options: {
eventDelegation: { eventDelegation: {
subSelector: '.tui-conversation-content', subSelector: '.tui-conversation-content'
}, }
}, }
}); })
} }
function onCurrentConversationUpdated(conversation: IConversationModel) { function onCurrentConversationUpdated(
currentConversation.value = conversation; conversation: IConversationModel
) {
currentConversation.value = conversation
}
/** 是否红包 */
const isRedEnvelope = (item: IConversationModel) => {
if (item?.lastMessage?.payload?.data) {
const businessID = JSON?.parse(
item?.lastMessage?.payload?.data
)?.businessID
return businessID === CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE
}
return false
}
/** 红包文案 */
const redEnvelopeText = (item: IConversationModel) => {
const payload = JSON.parse(item.lastMessage?.payload?.data)
const text = item.getLastMessage('text')?.split(':')
console.log(text)
if (text && text.length > 1) {
return `${text[0]}:[积分红包] ${payload.title}`
} else {
return `[积分红包] ${payload.title}`
}
} }
function onConversationListUpdated(list: IConversationModel[]) { function onConversationListUpdated(list: IConversationModel[]) {
conversationList.value = list; conversationList.value = list
} }
function onCurrentConversationIDUpdated(id: string) { function onCurrentConversationIDUpdated(id: string) {
currentConversationID.value = id; currentConversationID.value = id
} }
function onDisplayOnlineStatusUpdated(status: boolean) { function onDisplayOnlineStatusUpdated(status: boolean) {
displayOnlineStatus.value = status; displayOnlineStatus.value = status
} }
function onUserStatusListUpdated(list: Map<string, IUserStatus>) { function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list.size !== 0) { if (list.size !== 0) {
userOnlineStatusMap.value = [...list.entries()].reduce( userOnlineStatusMap.value = [...list.entries()].reduce(
(obj, [key, value]) => { (obj, [key, value]) => {
obj[key] = value; obj[key] = value
return obj; return obj
}, },
{} as IUserStatusMap, {} as IUserStatusMap
); )
} }
} }
// Expose to the parent component and close actionsMenu when a sliding event is detected // Expose to the parent component and close actionsMenu when a sliding event is detected
defineExpose({ closeChildren: closeConversationActionMenu }); defineExpose({ closeChildren: closeConversationActionMenu })
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -30,13 +30,13 @@
<Icon :file="rightIcon" /> <Icon :file="rightIcon" />
</span> </span>
</li> </li>
<li class="group-list-item" @click="cbPopupShow.open('bottom')"> <!-- <li class="group-list-item" @click="cbPopupShow.open('bottom')">
<label class="group-list-item-label">申请加群方式</label> <label class="group-list-item-label">申请加群方式</label>
<span class="group-h5-list-item-content"> <span class="group-h5-list-item-content">
<p class="content">{{ applyJoinOptionName }}</p> <p class="content">{{ applyJoinOptionName }}</p>
<Icon :file="rightIcon" /> <Icon :file="rightIcon" />
</span> </span>
</li> </li> -->
<li class="group-list-introduction"> <li class="group-list-introduction">
<div class="group-list-item"> <div class="group-list-item">
<label class="group-list-item-label"> <label class="group-list-item-label">
@@ -261,59 +261,55 @@
const createGroup = async (options: any) => { const createGroup = async (options: any) => {
try { try {
const data = { // const data = {
applyJoinOption: applyJoinOption.value, // applyJoinOption: applyJoinOption.value,
faceUrl: groupAvatar.value, // faceUrl: groupAvatar.value,
groupName: options.name, // groupName: options.name,
groupType: options.type, // groupType: options.type,
memberList: options.memberList.map((item: IUserProfile) => { // memberList: options.memberList.map((item: IUserProfile) => {
return { memberAccount: item.userID } // return { memberAccount: item.userID }
})
}
const res = await createImGroup(data)
const e = res.data
// const type = e.groupType
// if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) {
// await TUIGroupService.joinGroup({
// groupID: e.groupID,
// applyMessage: ''
// }) // })
// } // }
const newRes = await TUIGroupService.getGroupProfile({ // const res = await createImGroup(data)
groupID: e.groupId // const e = res.data
})
handleCompleteCreate(newRes.data.group) // const newRes = await TUIGroupService.getGroupProfile({
Toast({ // groupID: e.groupId
message: TUITranslateService.t('TUIGroup.群组创建成功'),
type: TOAST_TYPE.SUCCESS
})
// ========= 原本逻辑
// options.memberList = options.memberList.map(
// (item: IUserProfile) => {
// return { userID: item.userID }
// }
// )
// if (options.type === TUIChatEngine.TYPES.GRP_COMMUNITY) {
// delete options.groupID
// }
// const res = await TUIGroupService.createGroup(options)
// console.log(res)
// const { type } = res.data.group
// if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) {
// await TUIGroupService.joinGroup({
// groupID: res.data.group.groupID,
// applyMessage: ''
// }) // })
// }
// handleCompleteCreate(res.data.group) // handleCompleteCreate(newRes.data.group)
// Toast({ // Toast({
// message: TUITranslateService.t('TUIGroup.群组创建成功'), // message: TUITranslateService.t('TUIGroup.群组创建成功'),
// type: TOAST_TYPE.SUCCESS // type: TOAST_TYPE.SUCCESS
// }) // })
// ========= 原本逻辑
options.memberList = options.memberList.map(
(item: IUserProfile) => {
return { userID: item.userID }
}
)
if (options.type === TUIChatEngine.TYPES.GRP_COMMUNITY) {
delete options.groupID
}
const res = await TUIGroupService.createGroup({
...options,
avatar: groupAvatar.value
})
console.log(res)
const { type } = res.data.group
if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) {
await TUIGroupService.joinGroup({
groupID: res.data.group.groupID,
applyMessage: ''
})
}
handleCompleteCreate(res.data.group)
Toast({
message: TUITranslateService.t('TUIGroup.群组创建成功'),
type: TOAST_TYPE.SUCCESS
})
} catch (err: any) { } catch (err: any) {
Toast({ Toast({
message: err.message, message: err.message,

View File

@@ -238,7 +238,11 @@
import Server from '../server' import Server from '../server'
import { enableSampleTaskStatus } from '../../../utils/enableSampleTaskStatus' import { enableSampleTaskStatus } from '../../../utils/enableSampleTaskStatus'
import { IFriendProfile, IGroupMember } from '../../../interface' import { IFriendProfile, IGroupMember } from '../../../interface'
import { createImGroup, deleteImGroup, quitImGroup } from '../../../../api/tui-kit' import {
createImGroup,
deleteImGroup,
quitImGroup
} from '../../../../api/tui-kit'
const TUIGroupServer = Server.getInstance() const TUIGroupServer = Server.getInstance()
const TUIConstants = TUIGroupServer.constants const TUIConstants = TUIGroupServer.constants
@@ -400,7 +404,6 @@
const updateProfile = async (newGroupProfile: any) => { const updateProfile = async (newGroupProfile: any) => {
const { key, value } = newGroupProfile const { key, value } = newGroupProfile
if (key === 'muteAllMembers') {
const options: any = { const options: any = {
groupID: currentGroup.value.groupID, groupID: currentGroup.value.groupID,
[key]: value [key]: value
@@ -417,30 +420,47 @@
type: TOAST_TYPE.ERROR type: TOAST_TYPE.ERROR
}) })
}) })
} else { // if (key === 'muteAllMembers') {
const newKey = { // const options: any = {
notification: 'notification', // groupID: currentGroup.value.groupID,
name: 'groupName' // [key]: value
}[key] // }
const options: any = { // TUIGroupService.updateGroupProfile(options)
groupId: currentGroup.value.groupID, // .then((res: any) => {
[newKey]: value // console.log(res.data)
} // currentGroup.value = res.data.group
await createImGroup(options, 'put') // editLableName.value = ''
TUIGroupService.getGroupProfile({ // })
groupID: currentGroup.value.groupID // .catch((error: any) => {
}) // Toast({
.then((res: any) => { // message: error?.message,
currentGroup.value = res.data.group // type: TOAST_TYPE.ERROR
editLableName.value = '' // })
}) // })
.catch((error: any) => { // } else {
Toast({ // const newKey = {
message: error?.message, // notification: 'notification',
type: TOAST_TYPE.ERROR // name: 'groupName'
}) // }[key]
}) // const options: any = {
} // groupId: currentGroup.value.groupID,
// [newKey]: value
// }
// await createImGroup(options, 'put')
// TUIGroupService.getGroupProfile({
// groupID: currentGroup.value.groupID
// })
// .then((res: any) => {
// currentGroup.value = res.data.group
// editLableName.value = ''
// })
// .catch((error: any) => {
// Toast({
// message: error?.message,
// type: TOAST_TYPE.ERROR
// })
// })
// }
} }
const setCurrentTab = (tabName: string) => { const setCurrentTab = (tabName: string) => {
@@ -656,8 +676,8 @@
} }
const dismissGroup = async (group: any) => { const dismissGroup = async (group: any) => {
await deleteImGroup(group.groupID) // await deleteImGroup(group.groupID)
// await TUIGroupService.dismissGroup(group.groupID); await TUIGroupService.dismissGroup(group.groupID)
enableSampleTaskStatus('dismissGroup') enableSampleTaskStatus('dismissGroup')
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群组解散成功'), message: TUITranslateService.t('TUIGroup.群组解散成功'),
@@ -874,8 +894,8 @@
} }
const quitGroup = async (group: any) => { const quitGroup = async (group: any) => {
await quitImGroup(group.groupID) // await quitImGroup(group.groupID)
// await TUIGroupService.quitGroup(group.groupID) await TUIGroupService.quitGroup(group.groupID)
clearGroupInfo() clearGroupInfo()
} }

View File

@@ -18,6 +18,8 @@ export const CHAT_MSG_CUSTOM_TYPE = {
LINK: "text_link", LINK: "text_link",
CALL: 1, CALL: 1,
ORDER: "order", ORDER: "order",
/** 红包 */
RED_ENVELOPE: "redEnvelope"
}; };
export const DIALOG_CONTENT = { export const DIALOG_CONTENT = {

View File

@@ -40,3 +40,12 @@ export const deleteImGroupMember = (groupId, memberId) => {
data: { groupId, memberId } data: { groupId, memberId }
}) })
} }
/** 发红包 */
export const sendRedEnvelope = data => {
return http({
url: '/api/system/pointsRedPacket/send',
method: 'post',
data
})
}

View File

@@ -1,5 +1,4 @@
<template> <template>
<view>
<uni-popup ref="popup" type="bottom"> <uni-popup ref="popup" type="bottom">
<view class="modal"> <view class="modal">
<view class="title"> <view class="title">
@@ -42,7 +41,6 @@
</view> </view>
<view></view> <view></view>
</uni-popup> </uni-popup>
</view>
</template> </template>
<script> <script>
export default { export default {
@@ -55,9 +53,9 @@
}, },
methods: { methods: {
forgetPwd() { forgetPwd() {
uni.navigateTo({ // uni.navigateTo({
url: '/pages/myInfo/changePayPwd' // url: '/pages/myInfo/changePayPwd'
}) // })
}, },
open() { open() {
this.$refs.popup.open() this.$refs.popup.open()