475 lines
14 KiB
Vue
475 lines
14 KiB
Vue
<template>
|
|
<view ref="conversationListInnerDomRef" class="tui-conversation-list">
|
|
<ActionsMenu
|
|
v-if="isShowOverlay"
|
|
:selectedConversation="currentConversation"
|
|
:actionsMenuPosition="actionsMenuPosition"
|
|
:selectedConversationDomRect="currentConversationDomRect"
|
|
@closeConversationActionMenu="closeConversationActionMenu"
|
|
/>
|
|
<!-- <cb-empty
|
|
v-if="conversationList.length === 0"
|
|
name="您还没有好友"
|
|
></cb-empty> -->
|
|
<view
|
|
class="tui-conversation-item"
|
|
@click="navigateTo('/pages/my-index/set-up/message/index')"
|
|
>
|
|
<aside class="left">
|
|
<Avatar url="/static/images/message.svg" size="40px" />
|
|
<!-- 通知数量 -->
|
|
<!-- <span class="num">2</span> -->
|
|
<!-- <span class="num-notify" /> -->
|
|
</aside>
|
|
<view class="content">
|
|
<view class="content-header">
|
|
<view class="content-header-label">
|
|
<text class="name">消息通知</text>
|
|
</view>
|
|
<view class="middle-box">
|
|
<div class="middle-box-content">[系统消息]</div>
|
|
</view>
|
|
</view>
|
|
<!-- <view class="content-footer">
|
|
<span class="time">20秒前</span>
|
|
</view> -->
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-for="(conversation, index) in conversationList"
|
|
:id="`convlistitem-${index}`"
|
|
:key="index"
|
|
:class="[
|
|
'tui-conversation-content',
|
|
isMobile && 'tui-conversation-content-h5 disable-select'
|
|
]"
|
|
>
|
|
<div
|
|
:class="[
|
|
isPC && 'isPC',
|
|
'tui-conversation-item',
|
|
currentConversationID === conversation.conversationID &&
|
|
'tui-conversation-item-selected',
|
|
conversation.isPinned && 'tui-conversation-item-pinned'
|
|
]"
|
|
@click="enterConversationChat(conversation.conversationID)"
|
|
@longpress="
|
|
showConversationActionMenu($event, conversation, index)
|
|
"
|
|
@contextmenu="
|
|
showConversationActionMenu($event, conversation, index, true)
|
|
"
|
|
>
|
|
<aside class="left">
|
|
<Avatar
|
|
useSkeletonAnimation
|
|
:url="conversation.getAvatar()"
|
|
size="40px"
|
|
/>
|
|
<div
|
|
v-if="
|
|
userOnlineStatusMap && isShowUserOnlineStatus(conversation)
|
|
"
|
|
:class="[
|
|
'online-status',
|
|
Object.keys(userOnlineStatusMap).length > 0 &&
|
|
Object.keys(userOnlineStatusMap).includes(
|
|
conversation.userProfile.userID
|
|
) &&
|
|
userOnlineStatusMap[conversation.userProfile.userID]
|
|
.statusType === 1
|
|
? 'online-status-online'
|
|
: 'online-status-offline'
|
|
]"
|
|
/>
|
|
<span
|
|
v-if="conversation.unreadCount > 0 && !conversation.isMuted"
|
|
class="num"
|
|
>
|
|
{{
|
|
conversation.unreadCount > 99
|
|
? '99+'
|
|
: conversation.unreadCount
|
|
}}
|
|
</span>
|
|
<span
|
|
v-if="conversation.unreadCount > 0 && conversation.isMuted"
|
|
class="num-notify"
|
|
/>
|
|
</aside>
|
|
<view class="content">
|
|
<view class="content-header">
|
|
<label class="content-header-label">
|
|
<p class="name">{{ conversation.getShowName() }}</p>
|
|
</label>
|
|
<view v-if="isRedEnvelope(conversation)" class="middle-box">
|
|
<div class="middle-box-content">
|
|
{{ redEnvelopeText(conversation) }}
|
|
</div>
|
|
</view>
|
|
<view v-else class="middle-box">
|
|
<span
|
|
v-if="
|
|
conversation.draftText &&
|
|
conversation.conversationID !== currentConversationID
|
|
"
|
|
class="middle-box-draft"
|
|
>
|
|
{{ TUITranslateService.t('TUIChat.[草稿]') }}
|
|
</span>
|
|
<span
|
|
v-else-if="
|
|
conversation.type === 'GROUP' &&
|
|
conversation.groupAtInfoList &&
|
|
conversation.groupAtInfoList.length > 0
|
|
"
|
|
class="middle-box-at"
|
|
>
|
|
{{ conversation.getGroupAtInfo() }}
|
|
</span>
|
|
<view class="middle-box-content">
|
|
{{
|
|
isFirstCreateGroup(conversation) ||
|
|
conversation.getLastMessage('text')
|
|
}}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="content-footer">
|
|
<span class="time">
|
|
{{ conversation.getLastMessage('time') }}
|
|
</span>
|
|
<Icon
|
|
v-if="conversation.isMuted"
|
|
:file="muteIcon"
|
|
size="16px"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</div>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
interface IUserStatus {
|
|
statusType: number
|
|
customStatus: string
|
|
}
|
|
|
|
interface IUserStatusMap {
|
|
[userID: string]: IUserStatus
|
|
}
|
|
|
|
import { ref, onMounted, onUnmounted } from '../../../adapter-vue'
|
|
import TUIChatEngine, {
|
|
TUIStore,
|
|
StoreName,
|
|
TUIConversationService,
|
|
TUITranslateService,
|
|
IConversationModel
|
|
} from '@tencentcloud/chat-uikit-engine-lite'
|
|
import {
|
|
TUIGlobal,
|
|
isIOS,
|
|
addLongPressListener
|
|
} from '@tencentcloud/universal-api'
|
|
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'
|
|
import { navigateTo } from '../../../../utils/router'
|
|
|
|
const emits = defineEmits(['handleSwitchConversation', 'getPassingRef'])
|
|
const currentConversation = ref<IConversationModel>()
|
|
const currentConversationID = ref<string>()
|
|
const currentConversationDomRect = ref<DOMRect>()
|
|
const isShowOverlay = ref<boolean>(false)
|
|
const conversationList = ref<IConversationModel[]>([])
|
|
const conversationListDomRef = ref<HTMLElement | undefined>()
|
|
const conversationListInnerDomRef = ref<HTMLElement | undefined>()
|
|
const actionsMenuPosition = ref<{
|
|
top: number
|
|
left: number | undefined
|
|
conversationHeight: number | undefined
|
|
}>({
|
|
top: 0,
|
|
left: undefined,
|
|
conversationHeight: undefined
|
|
})
|
|
const displayOnlineStatus = ref(false)
|
|
const userOnlineStatusMap = ref<IUserStatusMap>()
|
|
|
|
let lastestOpenActionsMenuTime: number | null = null
|
|
|
|
onMounted(() => {
|
|
TUIStore.watch(StoreName.CONV, {
|
|
currentConversationID: onCurrentConversationIDUpdated,
|
|
conversationList: onConversationListUpdated,
|
|
currentConversation: onCurrentConversationUpdated
|
|
})
|
|
|
|
TUIStore.watch(StoreName.USER, {
|
|
displayOnlineStatus: onDisplayOnlineStatusUpdated,
|
|
userStatusList: onUserStatusListUpdated
|
|
})
|
|
|
|
if (!isUniFrameWork && isIOS && !isPC) {
|
|
addLongPressHandler()
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
TUIStore.unwatch(StoreName.CONV, {
|
|
currentConversationID: onCurrentConversationIDUpdated,
|
|
conversationList: onConversationListUpdated,
|
|
currentConversation: onCurrentConversationUpdated
|
|
})
|
|
|
|
TUIStore.unwatch(StoreName.USER, {
|
|
displayOnlineStatus: onDisplayOnlineStatusUpdated,
|
|
userStatusList: onUserStatusListUpdated
|
|
})
|
|
})
|
|
|
|
const isShowUserOnlineStatus = (
|
|
conversation: IConversationModel
|
|
): boolean => {
|
|
return (
|
|
displayOnlineStatus.value &&
|
|
conversation.type === TUIChatEngine.TYPES.CONV_C2C
|
|
)
|
|
}
|
|
|
|
const showConversationActionMenu = (
|
|
event: Event,
|
|
conversation: IConversationModel,
|
|
index: number,
|
|
isContextMenuEvent?: boolean
|
|
) => {
|
|
if (isContextMenuEvent) {
|
|
event.preventDefault()
|
|
if (isUniFrameWork) {
|
|
return
|
|
}
|
|
}
|
|
currentConversation.value = conversation
|
|
lastestOpenActionsMenuTime = Date.now()
|
|
getActionsMenuPosition(event, index)
|
|
}
|
|
|
|
const closeConversationActionMenu = () => {
|
|
// Prevent continuous triggering of overlay tap events
|
|
if (
|
|
lastestOpenActionsMenuTime &&
|
|
Date.now() - lastestOpenActionsMenuTime > 300
|
|
) {
|
|
currentConversation.value = undefined
|
|
isShowOverlay.value = false
|
|
}
|
|
}
|
|
|
|
const getActionsMenuPosition = (event: Event, index: number) => {
|
|
if (isUniFrameWork) {
|
|
if (typeof conversationListDomRef.value === 'undefined') {
|
|
emits('getPassingRef', conversationListDomRef)
|
|
}
|
|
const query = TUIGlobal?.createSelectorQuery().in(
|
|
conversationListDomRef.value
|
|
)
|
|
query
|
|
.select(`#convlistitem-${index}`)
|
|
.boundingClientRect(data => {
|
|
if (data) {
|
|
actionsMenuPosition.value = {
|
|
// The uni-page-head of uni-h5 is not considered a member of the viewport, so the height of the head is manually increased.
|
|
top: data.bottom - 44,
|
|
// @ts-expect-error in uniapp event has touches property
|
|
left: event.touches[0].pageX,
|
|
conversationHeight: data.height
|
|
}
|
|
isShowOverlay.value = true
|
|
}
|
|
})
|
|
.exec()
|
|
} else {
|
|
const rect =
|
|
(
|
|
(event.currentTarget || event.target) as HTMLElement
|
|
)?.getBoundingClientRect() || {}
|
|
if (rect) {
|
|
actionsMenuPosition.value = {
|
|
top: rect.bottom,
|
|
left: isPC ? (event as MouseEvent).clientX : undefined,
|
|
conversationHeight: rect.height
|
|
}
|
|
}
|
|
isShowOverlay.value = true
|
|
}
|
|
}
|
|
|
|
const enterConversationChat = (conversationID: string) => {
|
|
emits('handleSwitchConversation', conversationID)
|
|
TUIConversationService.switchConversation(conversationID)
|
|
}
|
|
|
|
function addLongPressHandler() {
|
|
if (!conversationListInnerDomRef.value) {
|
|
return
|
|
}
|
|
addLongPressListener({
|
|
element: conversationListInnerDomRef.value,
|
|
onLongPress: (event, target) => {
|
|
const index = (
|
|
Array.from(
|
|
conversationListInnerDomRef.value!.children
|
|
) as HTMLElement[]
|
|
).indexOf(target!)
|
|
showConversationActionMenu(
|
|
event,
|
|
conversationList.value[index],
|
|
index
|
|
)
|
|
},
|
|
options: {
|
|
eventDelegation: {
|
|
subSelector: '.tui-conversation-content'
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function onCurrentConversationUpdated(
|
|
conversation: IConversationModel
|
|
) {
|
|
currentConversation.value = conversation
|
|
}
|
|
|
|
/** 是否红包 */
|
|
const isRedEnvelope = (item: IConversationModel) => {
|
|
if (
|
|
item?.lastMessage?.payload?.data &&
|
|
item?.lastMessage?.type === 'TIMCustomElem'
|
|
) {
|
|
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(':')
|
|
|
|
if (text && text.length > 1) {
|
|
return `${text[0]}:[积分红包] ${payload.title}`
|
|
} else {
|
|
return `[积分红包] ${payload.title}`
|
|
}
|
|
}
|
|
|
|
/** 是否最开始创建群聊 */
|
|
const isFirstCreateGroup = (item: IConversationModel) => {
|
|
// if (item.type === 'GROUP' && item?.lastMessage?.payload?.data) {
|
|
// const data = JSON.parse(item?.lastMessage?.payload?.data)
|
|
// return data.content === 'Create Group'
|
|
// ? `${item.getLastMessage('text')?.split(':')[0]}:创建群聊`
|
|
// : ''
|
|
// }
|
|
if (item?.lastMessage?.payload?.data) {
|
|
const data = JSON.parse(item?.lastMessage?.payload?.data)
|
|
const text = item.getLastMessage('text')?.split(':')
|
|
const isText = text && text.length > 1
|
|
if (data.businessID === CHAT_MSG_CUSTOM_TYPE.GOODS) {
|
|
if (isText) {
|
|
return `${text[0]}:[商品信息] ${data.title}`
|
|
} else {
|
|
return `[商品信息]:${data.title}`
|
|
}
|
|
}
|
|
if (data.businessID === CHAT_MSG_CUSTOM_TYPE.LIVE) {
|
|
if (isText) {
|
|
return `${text[0]}:[直播信息] ${data.title}`
|
|
} else {
|
|
return `[直播信息]:${data.title}`
|
|
}
|
|
}
|
|
if (data.businessID === CHAT_MSG_CUSTOM_TYPE.VOICE_CALL) {
|
|
if (isText) {
|
|
return `${text[0]}:[语音通话]`
|
|
} else {
|
|
return `[语音通话]`
|
|
}
|
|
}
|
|
if (data.businessID === CHAT_MSG_CUSTOM_TYPE.VIDEO_CALL) {
|
|
if (isText) {
|
|
return `${text[0]}:[视频通话]`
|
|
} else {
|
|
return `[视频通话]`
|
|
}
|
|
}
|
|
if (data.content === 'Create Group' && item.type === 'GROUP') {
|
|
return `${item.getLastMessage('text')?.split(':')[0]}:创建群聊`
|
|
}
|
|
return ''
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function onConversationListUpdated(list: IConversationModel[]) {
|
|
conversationList.value = list
|
|
}
|
|
|
|
function onCurrentConversationIDUpdated(id: string) {
|
|
currentConversationID.value = id
|
|
}
|
|
|
|
function onDisplayOnlineStatusUpdated(status: boolean) {
|
|
displayOnlineStatus.value = status
|
|
}
|
|
|
|
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
|
|
if (list.size !== 0) {
|
|
userOnlineStatusMap.value = [...list.entries()].reduce(
|
|
(obj, [key, value]) => {
|
|
obj[key] = value
|
|
return obj
|
|
},
|
|
{} as IUserStatusMap
|
|
)
|
|
}
|
|
}
|
|
// Expose to the parent component and close actionsMenu when a sliding event is detected
|
|
defineExpose({ closeChildren: closeConversationActionMenu })
|
|
</script>
|
|
|
|
<style lang="scss" scoped src="./style/index.scss"></style>
|
|
<style lang="scss" scoped>
|
|
.disable-select {
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
-khtml-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
}
|
|
.tui-conversation-item {
|
|
&::after {
|
|
display: none !important;
|
|
}
|
|
&:active {
|
|
background: #f4f4f4 !important;
|
|
}
|
|
}
|
|
</style>
|