311 lines
9.5 KiB
Vue
311 lines
9.5 KiB
Vue
<template>
|
|
<div
|
|
ref="conversationListInnerDomRef"
|
|
class="tui-conversation-list"
|
|
>
|
|
<ActionsMenu
|
|
v-if="isShowOverlay"
|
|
:selectedConversation="currentConversation"
|
|
:actionsMenuPosition="actionsMenuPosition"
|
|
:selectedConversationDomRect="currentConversationDomRect"
|
|
@closeConversationActionMenu="closeConversationActionMenu"
|
|
/>
|
|
<div
|
|
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="30px"
|
|
/>
|
|
<div
|
|
v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)"
|
|
:class="[
|
|
'online-status',
|
|
Object.keys(userOnlineStatusMap).length > 0 &&
|
|
Object.keys(userOnlineStatusMap).includes(
|
|
conversation.userProfile.userID
|
|
) &&
|
|
userOnlineStatusMap[conversation.userProfile.userID]
|
|
.statusType === 1
|
|
? '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>
|
|
<div class="content">
|
|
<div class="content-header">
|
|
<label class="content-header-label">
|
|
<p class="name">{{ conversation.getShowName() }}</p>
|
|
</label>
|
|
<div class="middle-box">
|
|
<span
|
|
v-if="conversation.draftText && conversation.conversationID !== currentConversationID"
|
|
class="middle-box-draft"
|
|
>{{ TUITranslateService.t('TUIChat.[草稿]') }}</span>
|
|
<span
|
|
v-else-if="
|
|
conversation.type === 'GROUP' &&
|
|
conversation.groupAtInfoList &&
|
|
conversation.groupAtInfoList.length > 0
|
|
"
|
|
class="middle-box-at"
|
|
>{{ conversation.getGroupAtInfo() }}</span>
|
|
<div class="middle-box-content">
|
|
{{ conversation.getLastMessage("text") }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="content-footer">
|
|
<span class="time">{{ conversation.getLastMessage("time") }}</span>
|
|
<Icon
|
|
v-if="conversation.isMuted"
|
|
:file="muteIcon"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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';
|
|
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';
|
|
|
|
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 + (isH5 ? 44 : 0),
|
|
// @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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
</style>
|