123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- <template>
- <div class="message-bubble" :class="[message.flow === 'in' ? '' : 'reverse']" ref="htmlRefHook">
- <img class="avatar" :src="message?.avatar || 'https://oss.dayaedu.com/news-info/07/1690787574969.png'" onerror="this.src='https://oss.dayaedu.com/news-info/07/1690787574969.png'" />
- <main class="message-area">
- <label class="name" v-if="message.flow === 'in' && message.conversationType === 'GROUP'">
- {{ message.nameCard || message.nick || message.from }}
- </label>
- <div :class="handleImageOrVideoBubbleStyle(message)" @click.prevent.right="toggleDialog">
- <div class="message-replie-area" :class="[message?.flow === 'in' ? '' : 'message-replies-area-reverse']" v-if="message?.cloudCustomData && referenceMessage && referenceMessage?.messageRootID" @click="showRepliesDialog(message, false)">
- <MessageReference :message="message" :referenceMessage="referenceMessage" :referenceForShow="referenceForShow" :url="url" :face="face" :allMessageID="allMessageID" type="reply" />
- </div>
- <slot />
- <div v-if="dropdown" ref="dropdownRef" class="dropdown-inner">
- <div class="dialog" :class="[message.flow === 'in' ? '' : 'dialog-right']" @click="dropdown = false">
- <slot name="dialog" />
- </div>
- </div>
- <MessageEmojiReact :message="message" type="content" v-if="needEmojiReact && isEmojiReactionInMessage(message)" />
- </div>
- </main>
- <label class="message-label fail" v-if="message.status === 'fail'" @click="resendMessage(message)"> ! </label>
- <!-- <label
- class="message-label"
- :class="readReceiptStyle(message)"
- v-if="showReadReceiptTag(message)"
- @click="showReadReceiptDialog(message)"
- >
- <span>{{ readReceiptCont(message) }}</span>
- </label> -->
- </div>
- <div class="message-reference-area" :class="[message.flow === 'in' ? '' : 'message-reference-area-reverse']" v-if="message?.cloudCustomData && referenceMessage && !referenceMessage?.messageRootID" @click="jumpToAim(referenceMessage)">
- <MessageReference :message="message" :referenceMessage="referenceMessage" :referenceForShow="referenceForShow" :url="url" :face="face" :allMessageID="allMessageID" type="reference" />
- </div>
- <label class="message-replies" :class="[message.flow === 'in' ? '' : 'message-replies-reverse']" v-if="replies?.length" @click="showRepliesDialog(message, true)">
- <i class="icon icon-msg-replies"></i>
- <span>{{ replies?.length + $t("TUIChat.条回复") }}</span>
- </label>
- </template>
- <script lang="ts">
- import { decodeText } from "../utils/decodeText";
- import constant from "../../constant";
- import { defineComponent, watchEffect, reactive, toRefs, ref, nextTick, watch } from "vue";
- import { onClickOutside, onLongPress, useElementBounding } from "@vueuse/core";
- import { deepCopy, JSONToObject } from "../utils/utils";
- import { handleErrorPrompts } from "../../utils";
- import TUIChat from "../index.vue";
- import MessageReference from "./message-reference.vue";
- import { Message } from "../interface";
- import { TUIEnv } from "../../../../TUIPlugin";
- import MessageEmojiReact from "./message-emoji-react.vue";
- import TIM from "../../../../TUICore/tim/index";
- const messageBubble = defineComponent({
- props: {
- data: {
- type: Object,
- default: () => ({}),
- },
- messagesList: {
- type: Array,
- default: () => [],
- },
- isH5: {
- type: Boolean,
- default: false,
- },
- needGroupReceipt: {
- type: Boolean,
- default: false,
- },
- needReplies: {
- type: Boolean,
- default: true,
- },
- flow: {
- type: String,
- default: "",
- },
- needEmojiReact: {
- type: Boolean,
- default: false,
- },
- },
- emits: ["jumpID", "resendMessage", "showReadReceiptDialog", "showRepliesDialog", "dropDownOpen"],
- components: {
- MessageReference,
- MessageEmojiReact,
- },
- setup(props: any, ctx: any) {
- const { t } = (window as any).TUIKitTUICore.config.i18n.useI18n();
- const { TUIServer } = TUIChat;
- const data = reactive({
- env: TUIEnv(),
- message: {} as Message,
- messagesList: [],
- show: false,
- type: {},
- referenceMessage: {},
- referenceForShow: {},
- allMessageID: "",
- needGroupReceipt: false,
- needReplies: true,
- replies: [],
- face: [],
- url: "",
- needEmojiReact: false,
- });
- watchEffect(() => {
- data.type = constant;
- data.messagesList = props.messagesList;
- data.needEmojiReact = props.needEmojiReact;
- data.message = deepCopy(data.messagesList?.find((item: any) => (item as any)?.ID === props.message?.ID) || props.data);
- data.needGroupReceipt = props.needGroupReceipt;
- data.needReplies = props.needReplies;
- if ((data.message as any).cloudCustomData) {
- const messageIDList: any[] = [];
- const cloudCustomData = JSONToObject((data.message as any).cloudCustomData);
- data.replies = cloudCustomData?.messageReplies?.replies || [];
- data.referenceMessage = cloudCustomData.messageReply ? cloudCustomData.messageReply : "";
- for (let index = 0; index < (data.messagesList as any).length; index++) {
- // To determine whether the referenced message is still in the message list, the corresponding field of the referenced message is displayed if it is in the message list. Otherwise, messageabstract/messagesender is displayed
- messageIDList.push((data.messagesList as any)[index].ID);
- (data as any).allMessageID = JSON.stringify(messageIDList);
- if ((data.messagesList as any)[index].ID === (data.referenceMessage as any)?.messageID) {
- data.referenceForShow = (data.messagesList as any)[index];
- if ((data.referenceMessage as any).messageType === constant.typeText) {
- (data as any).face = decodeText((data.referenceForShow as any).payload);
- }
- if ((data.referenceMessage as any).messageType === constant.typeFace) {
- (data as any).url = `https://web.sdk.qcloud.com/im/assets/face-elem/${(data.referenceForShow as any).payload.data}@2x.png`;
- }
- }
- }
- } else {
- data.replies = [];
- }
- });
- const htmlRefHook = ref<HTMLElement | null>(null);
- const dropdown = ref(false);
- const dropdownRef = ref(null);
- const toggleDialog = (e: any) => {
- dropdown.value = !dropdown.value;
- if (dropdown.value) {
- ctx.emit("dropDownOpen", dropdownRef);
- nextTick(() => {
- const dialogDom = (dropdownRef as any)?.value?.children[0];
- const dialogElement = document.getElementsByClassName("dialog-item")[0] as HTMLElement;
- const parentDom = (dropdownRef as any)?.value?.offsetParent;
- const parentBound = useElementBounding(parentDom);
- const messageListDom = document.getElementById("messageEle") as HTMLElement;
- const messageListBound = useElementBounding(messageListDom);
- const leftRange = messageListBound?.left?.value;
- const rightRange = messageListBound?.left?.value + (messageListDom as any).clientWidth - dialogDom.clientWidth + 76;
- const topRange = messageListBound?.top?.value;
- const bottomRange = messageListBound?.top?.value + (messageListDom as any).clientHeight - dialogDom.clientHeight;
- const { clientX, clientY } = e;
- if (data?.env?.isH5) {
- if (parentBound?.top?.value <= dialogElement?.clientHeight) {
- dialogDom.style.bottom = `-${dialogElement?.clientHeight}Px`;
- } else {
- if (data?.message?.flow === "in") {
- dialogDom.style.top = `-${dialogElement?.clientHeight - 20}Px`;
- } else {
- dialogDom.style.top = `-${dialogElement?.clientHeight}Px`;
- }
- }
- const centerWidth = parentBound?.left?.value + parentBound?.width?.value / 2;
- if (centerWidth > dialogElement.clientWidth / 2 && centerWidth < messageListDom?.clientWidth - dialogElement.clientWidth / 2) {
- dialogDom.style.left = "calc(50% - 135Px)";
- } else if (centerWidth <= dialogElement.clientWidth / 2) {
- dialogDom.style.left = "-20Px";
- } else {
- dialogDom.style.left = `-${dialogElement.clientWidth / 2 + 10}Px`;
- }
- return;
- }
- switch (true) {
- case clientX > leftRange && clientX < rightRange:
- dialogDom.style.left = `${Math.max(e.clientX - parentBound?.left?.value - 76, -40)}Px`;
- break;
- case clientX <= leftRange:
- dialogDom.style.left = "20Px";
- break;
- case clientX >= rightRange:
- dialogDom.style.right = `${Math.max(parentBound?.left?.value + parentDom?.clientWidth - e.clientX - 256, -10)}Px`;
- break;
- }
- switch (true) {
- case clientY > topRange && clientY < bottomRange:
- dialogDom.style.top = `${e.clientY - parentBound?.top?.value}Px`;
- dialogDom.style.cssText = dialogDom.style.cssText.replace("align-items:end;", "");
- break;
- case clientY <= topRange:
- dialogDom.style.top = "0Px";
- dialogDom.style.cssText = dialogDom.style.cssText.replace("align-items:end;", "");
- break;
- case clientY >= bottomRange:
- dialogDom.style.bottom = `${parentBound?.top?.value + parentDom?.clientHeight - e.clientY}Px`;
- dialogDom.style.cssText += "align-items:end;";
- break;
- }
- });
- }
- };
- const jumpToAim = (message: any) => {
- if ((data.referenceMessage as any)?.messageID && data.allMessageID.includes((data.referenceMessage as any)?.messageID)) {
- ctx.emit("jumpID", (data.referenceMessage as any).messageID);
- } else {
- const message = t("TUIChat.无法定位到原消息");
- handleErrorPrompts(message, props);
- }
- };
- onClickOutside(dropdownRef, () => {
- dropdown.value = false;
- });
- const toggleDialogH5 = (e: any) => {
- if (data?.env?.isH5) toggleDialog(e);
- return;
- };
- onLongPress(htmlRefHook, toggleDialogH5);
- const resendMessage = (message: any) => {
- ctx.emit("resendMessage", message);
- };
- const showReadReceiptTag = (message: any) => {
- if (message.flow === "out" && message.status === "success" && message.needReadReceipt) {
- return true;
- }
- return false;
- };
- const readReceiptStyle = (message: any) => {
- if (message?.readReceiptInfo?.isPeerRead || (message?.readReceiptInfo?.isPeerRead === undefined && message?.isPeerRead) || message?.readReceiptInfo?.unreadCount === 0) {
- return "";
- }
- return "unRead";
- };
- const readReceiptCont = (message: any) => {
- switch (message.conversationType) {
- case TUIServer.TUICore.TIM.TYPES.CONV_C2C:
- if (message?.readReceiptInfo?.isPeerRead || (message?.readReceiptInfo?.isPeerRead === undefined && message?.isPeerRead)) {
- return t("TUIChat.已读");
- }
- return t("TUIChat.未读");
- case TUIServer.TUICore.TIM.TYPES.CONV_GROUP:
- if (message.readReceiptInfo.unreadCount === 0) {
- return t("TUIChat.全部已读");
- }
- if (message.readReceiptInfo.readCount === 0 || (message.readReceiptInfo.unreadCount === undefined && message.readReceiptInfo.readCount === undefined)) {
- return t("TUIChat.未读");
- }
- return `${message.readReceiptInfo.readCount + t("TUIChat.人已读")}`;
- default:
- return "";
- }
- };
- const showReadReceiptDialog = (message: any) => {
- ctx.emit("showReadReceiptDialog", message, "receipt");
- };
- const showRepliesDialog = (message: any, isRoot: boolean) => {
- if (isRoot) {
- ctx.emit("showRepliesDialog", message, "replies");
- return;
- }
- if ((data.referenceMessage as any)?.messageRootID) {
- const message = data.messagesList?.find((item: Message) => item.ID === (data.referenceMessage as any)?.messageRootID);
- if (message) {
- ctx.emit("showRepliesDialog", message, "replies");
- return;
- } else {
- const message = t("TUIChat.无法定位到原消息");
- handleErrorPrompts(message, props);
- }
- }
- };
- const handleImageOrVideoBubbleStyle = (message: Message) => {
- const classNameList = ["content"];
- if (!message) return classNameList;
- classNameList.push(`content-${data.message.flow}`);
- if (data.message.type === TIM.TYPES.MSG_IMAGE && !isEmojiReactionInMessage(message)) {
- classNameList.push("content-image");
- }
- if (data.message.type === TIM.TYPES.MSG_VIDEO && !isEmojiReactionInMessage(message)) {
- classNameList.push("content-video");
- }
- return classNameList;
- };
- const isEmojiReactionInMessage = (message: Message) => {
- try {
- if (!message?.cloudCustomData) return;
- const reactList = JSONToObject(message?.cloudCustomData)?.messageReact?.reacts;
- if (!reactList || Object.keys(reactList).length === 0) return false;
- return true;
- } catch (err) {
- console.warn(err);
- return false;
- }
- };
- return {
- ...toRefs(data),
- toggleDialog,
- htmlRefHook,
- jumpToAim,
- dropdown,
- dropdownRef,
- resendMessage,
- showReadReceiptTag,
- readReceiptStyle,
- readReceiptCont,
- showReadReceiptDialog,
- showRepliesDialog,
- handleImageOrVideoBubbleStyle,
- isEmojiReactionInMessage,
- TIM,
- };
- },
- });
- export default messageBubble;
- </script>
- <style lang="scss" scoped>
- @import url("../../../styles/common.scss");
- @import url("../../../styles/icon.scss");
- .reverse {
- flex-direction: row-reverse;
- justify-content: flex-start;
- }
- .avatar {
- width: 36px;
- height: 36px;
- border-radius: 5px;
- }
- .message-bubble {
- width: 100%;
- display: flex;
- padding-bottom: 5px;
- }
- .line-left {
- border: 1px solid rgba(0, 110, 255, 0.5);
- }
- .message-reference-area {
- display: flex;
- background: #f2f2f2;
- border-radius: 0.5rem;
- border-radius: 0.63rem;
- align-self: start;
- margin-left: 44px;
- margin-right: 8px;
- &-show {
- width: 100%;
- display: flex;
- flex-direction: inherit;
- justify-content: center;
- padding: 6px;
- p {
- font-family: PingFangSC-Regular;
- font-weight: 400;
- font-size: 0.88rem;
- color: #999999;
- letter-spacing: 0;
- word-break: keep-all;
- padding-right: 5px;
- }
- span {
- height: 1.25rem;
- font-family: PingFangSC-Regular;
- font-weight: 400;
- font-size: 0.88rem;
- color: #999999;
- letter-spacing: 0;
- display: inline-block;
- }
- }
- }
- .message-replies {
- display: flex;
- align-self: start;
- margin-left: 44px;
- margin-right: 8px;
- padding: 2px;
- color: #999999;
- font-size: 10px;
- i {
- margin: 4px;
- }
- span {
- line-height: 20px;
- }
- }
- .message-reference-area-reverse,
- .message-replies-reverse {
- align-self: end;
- margin-right: 44px;
- margin-left: 8px;
- }
- .message-img {
- max-width: min(calc(100vw - 180px), 300px);
- max-height: min(calc(100vw - 180px), 300px);
- }
- .message-video-cover {
- display: inline-block;
- position: relative;
- &::before {
- position: absolute;
- z-index: 1;
- content: "";
- width: 0px;
- height: 0px;
- border: 15px solid transparent;
- border-left: 20px solid #ffffff;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- margin: auto;
- }
- }
- .message-videoimg {
- max-width: min(calc(100vw - 160px), 300px);
- max-height: min(calc(100vw - 160px), 300px);
- }
- .face-box {
- display: flex;
- align-items: center;
- }
- .text-img {
- width: 20px;
- height: 20px;
- }
- .message-audio {
- padding-left: 10px;
- display: flex;
- align-items: center;
- position: relative;
- .icon {
- margin: 0 4px;
- }
- audio {
- width: 0;
- height: 0;
- }
- }
- .reserve {
- flex-direction: row-reverse;
- }
- .message-area {
- max-width: calc(100% - 54px);
- position: relative;
- display: flex;
- flex-direction: column;
- padding: 0 8px;
- .name {
- padding-bottom: 4px;
- font-weight: 400;
- font-size: 0.8rem;
- color: #999999;
- letter-spacing: 0;
- }
- .reference-content {
- padding: 12px;
- font-weight: 400;
- font-size: 14px;
- color: burlywood;
- letter-spacing: 0;
- word-wrap: break-word;
- word-break: break-all;
- animation: reference 800ms;
- }
- @-webkit-keyframes reference {
- from {
- opacity: 1;
- }
- 50% {
- background-color: #ff9c19;
- }
- to {
- opacity: 1;
- }
- }
- @keyframes reference {
- from {
- opacity: 1;
- }
- 50% {
- background-color: #ff9c19;
- }
- to {
- opacity: 1;
- }
- }
- .content {
- padding: 12px;
- font-weight: 400;
- font-size: 14px;
- color: #000000;
- letter-spacing: 0;
- word-wrap: break-word;
- word-break: break-all;
- width: fit-content;
- &-in {
- background: #fbfbfb;
- border-radius: 10px 10px 10px 10px;
- }
- &-out {
- background: #FFF;
- border-radius: 10px 0px 10px 10px;
- }
- &-image {
- padding: 0px;
- height: fit-content;
- border-radius: 10px 0px 10px 10px;
- }
- &-video {
- padding: 0px;
- height: fit-content;
- background: transparent;
- border-radius: 10px;
- }
- }
- }
- .message-label {
- align-self: flex-end;
- font-family: PingFangSC-Regular;
- font-weight: 400;
- font-size: 12px;
- color: #b6b8ba;
- word-break: keep-all;
- }
- .fail {
- width: 15px;
- height: 15px;
- border-radius: 15px;
- background: red;
- color: #ffffff;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .unRead {
- color: #679ce1;
- }
- .dropdown-inner {
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- }
- .dialog {
- position: absolute;
- z-index: 1;
- display: flex;
- flex-direction: row;
- width: fit-content;
- &-right {
- right: 0;
- }
- }
- </style>
|