devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
530 lines (529 loc) • 22 kB
JavaScript
/**
* DevExtreme (esm/__internal/ui/chat/messagelist.js)
* Version: 24.2.7
* Build date: Mon Apr 28 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import _extends from "@babel/runtime/helpers/esm/extends";
import {
Guid
} from "../../../common";
import dateLocalization from "../../../common/core/localization/date";
import messageLocalization from "../../../common/core/localization/message";
import $ from "../../../core/renderer";
import resizeObserverSingleton from "../../../core/resize_observer";
import {
noop
} from "../../../core/utils/common";
import dateUtils from "../../../core/utils/date";
import dateSerialization from "../../../core/utils/date_serialization";
import {
isElementInDom
} from "../../../core/utils/dom";
import {
getHeight
} from "../../../core/utils/size";
import {
isDate,
isDefined
} from "../../../core/utils/type";
import ScrollView from "../../../ui/scroll_view";
import Widget from "../../core/widget/widget";
import {
getScrollTopMax
} from "../../ui/scroll_view/utils/get_scroll_top_max";
import {
isElementVisible
} from "../splitter/utils/layout";
import MessageBubble, {
CHAT_MESSAGEBUBBLE_CLASS
} from "./messagebubble";
import MessageGroup, {
CHAT_MESSAGEGROUP_ALIGNMENT_END_CLASS,
CHAT_MESSAGEGROUP_ALIGNMENT_START_CLASS,
CHAT_MESSAGEGROUP_CLASS,
MESSAGE_DATA_KEY
} from "./messagegroup";
import TypingIndicator from "./typingindicator";
const CHAT_MESSAGELIST_CLASS = "dx-chat-messagelist";
const CHAT_MESSAGELIST_CONTENT_CLASS = "dx-chat-messagelist-content";
const CHAT_MESSAGELIST_EMPTY_CLASS = "dx-chat-messagelist-empty";
const CHAT_MESSAGELIST_EMPTY_LOADING_CLASS = "dx-chat-messagelist-empty-loading";
const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = "dx-chat-messagelist-empty-view";
const CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS = "dx-chat-messagelist-empty-image";
const CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS = "dx-chat-messagelist-empty-message";
const CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS = "dx-chat-messagelist-empty-prompt";
const CHAT_MESSAGELIST_DAY_HEADER_CLASS = "dx-chat-messagelist-day-header";
const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS = "dx-chat-last-messagegroup-alignment-start";
const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS = "dx-chat-last-messagegroup-alignment-end";
const SCROLLABLE_CONTAINER_CLASS = "dx-scrollable-container";
export const MESSAGEGROUP_TIMEOUT = 3e5;
class MessageList extends Widget {
_getDefaultOptions() {
return _extends({}, super._getDefaultOptions(), {
items: [],
currentUserId: "",
showDayHeaders: true,
dayHeaderFormat: "shortdate",
messageTimestampFormat: "shorttime",
typingUsers: [],
isLoading: false,
showAvatar: true,
showUserName: true,
showMessageTimestamp: true,
messageTemplate: null
})
}
_init() {
super._init();
this._lastMessageDate = null
}
_initMarkup() {
$(this.element()).addClass("dx-chat-messagelist");
super._initMarkup();
this._renderScrollView();
this._renderMessageListContent();
this._toggleEmptyView();
this._renderMessageGroups();
this._renderTypingIndicator();
this._updateAria();
this._scrollDownContent()
}
_renderContentImpl() {
super._renderContentImpl();
this._attachResizeObserverSubscription()
}
_attachResizeObserverSubscription() {
const element = this.$element().get(0);
resizeObserverSingleton.unobserve(element);
resizeObserverSingleton.observe(element, (entry => this._resizeHandler(entry)))
}
_resizeHandler(_ref) {
let {
contentRect: contentRect,
target: target
} = _ref;
if (!isElementInDom($(target)) || !isElementVisible(target)) {
return
}
const isInitialRendering = !isDefined(this._containerClientHeight);
const newHeight = contentRect.height;
if (isInitialRendering) {
this._scrollDownContent()
} else {
const heightChange = this._containerClientHeight - newHeight;
const isHeightDecreasing = heightChange > 0;
let scrollTop = this._scrollView.scrollTop();
if (isHeightDecreasing) {
scrollTop += heightChange;
this._scrollView.scrollTo({
top: scrollTop
})
}
}
this._containerClientHeight = newHeight
}
_renderEmptyViewContent() {
const $emptyView = $("<div>").addClass("dx-chat-messagelist-empty-view").attr("id", `dx-${new Guid}`);
$("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-image");
const messageText = messageLocalization.format("dxChat-emptyListMessage");
$("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-message").text(messageText);
const promptText = messageLocalization.format("dxChat-emptyListPrompt");
$("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-prompt").text(promptText);
$emptyView.appendTo(this._$content)
}
_renderTypingIndicator() {
const {
typingUsers: typingUsers
} = this.option();
const $typingIndicator = $("<div>").appendTo(this._$scrollViewContent());
this._typingIndicator = this._createComponent($typingIndicator, TypingIndicator, {
typingUsers: typingUsers
})
}
_isEmpty() {
const {
items: items
} = this.option();
return 0 === items.length
}
_isCurrentUser(id) {
const {
currentUserId: currentUserId
} = this.option();
return currentUserId === id
}
_messageGroupAlignment(id) {
return this._isCurrentUser(id) ? "end" : "start"
}
_createMessageGroupComponent(items, userId) {
const {
showAvatar: showAvatar,
showUserName: showUserName,
showMessageTimestamp: showMessageTimestamp,
messageTemplate: messageTemplate,
messageTimestampFormat: messageTimestampFormat
} = this.option();
const $messageGroup = $("<div>").appendTo(this._$content);
this._createComponent($messageGroup, MessageGroup, {
items: items,
alignment: this._messageGroupAlignment(userId),
showAvatar: showAvatar,
showUserName: showUserName,
showMessageTimestamp: showMessageTimestamp,
messageTemplate: messageTemplate,
messageTimestampFormat: messageTimestampFormat
})
}
_renderScrollView() {
const $scrollable = $("<div>").appendTo(this.$element());
this._scrollView = this._createComponent($scrollable, ScrollView, {
useKeyboard: false,
bounceEnabled: false,
reachBottomText: "",
indicateLoading: false,
onReachBottom: noop
})
}
_shouldAddDayHeader(timestamp) {
const {
showDayHeaders: showDayHeaders
} = this.option();
if (!showDayHeaders) {
return false
}
const deserializedDate = dateSerialization.deserializeDate(timestamp);
if (!isDate(deserializedDate) || isNaN(deserializedDate.getTime())) {
return false
}
return !dateUtils.sameDate(this._lastMessageDate, deserializedDate)
}
_createDayHeader(timestamp) {
const deserializedDate = dateSerialization.deserializeDate(timestamp);
const today = new Date;
const yesterday = new Date((new Date).setDate(today.getDate() - 1));
const {
dayHeaderFormat: dayHeaderFormat
} = this.option();
this._lastMessageDate = deserializedDate;
let headerDate = dateLocalization.format(deserializedDate, dayHeaderFormat);
if (dateUtils.sameDate(deserializedDate, today)) {
headerDate = `${messageLocalization.format("Today")} ${headerDate}`
}
if (dateUtils.sameDate(deserializedDate, yesterday)) {
headerDate = `${messageLocalization.format("Yesterday")} ${headerDate}`
}
$("<div>").addClass("dx-chat-messagelist-day-header").text(headerDate).appendTo(this._$content)
}
_updateLoadingState(isLoading) {
if (!this._scrollView) {
return
}
this.$element().toggleClass("dx-chat-messagelist-empty-loading", this._isEmpty() && isLoading);
this._scrollView.release(!isLoading)
}
_renderMessageListContent() {
this._$content = $("<div>").addClass("dx-chat-messagelist-content").appendTo(this._$scrollViewContent())
}
_toggleEmptyView() {
this._getEmptyView().remove();
const {
isLoading: isLoading
} = this.option();
this.$element().toggleClass("dx-chat-messagelist-empty", this._isEmpty() && !isLoading).toggleClass("dx-chat-messagelist-empty-loading", this._isEmpty() && isLoading);
if (this._isEmpty() && !isLoading) {
this._renderEmptyViewContent();
this._updateLoadingState(false)
}
}
_renderMessageGroups() {
var _items$;
const {
isLoading: isLoading,
items: items
} = this.option();
if (this._isEmpty() && !isLoading) {
return
}
let currentMessageGroupUserId = null === (_items$ = items[0]) || void 0 === _items$ || null === (_items$ = _items$.author) || void 0 === _items$ ? void 0 : _items$.id;
let currentMessageGroupItems = [];
items.forEach(((item, index) => {
var _newMessageGroupItem$;
const newMessageGroupItem = item ?? {};
const id = null === (_newMessageGroupItem$ = newMessageGroupItem.author) || void 0 === _newMessageGroupItem$ ? void 0 : _newMessageGroupItem$.id;
const shouldCreateDayHeader = this._shouldAddDayHeader(newMessageGroupItem.timestamp);
const isTimeoutExceeded = this._isTimeoutExceeded(currentMessageGroupItems[currentMessageGroupItems.length - 1] ?? {}, item);
const shouldCreateMessageGroup = shouldCreateDayHeader && currentMessageGroupItems.length || isTimeoutExceeded || id !== currentMessageGroupUserId;
if (shouldCreateMessageGroup) {
this._createMessageGroupComponent(currentMessageGroupItems, currentMessageGroupUserId);
currentMessageGroupUserId = id;
currentMessageGroupItems = [];
currentMessageGroupItems.push(newMessageGroupItem)
} else {
currentMessageGroupItems.push(newMessageGroupItem)
}
if (shouldCreateDayHeader) {
this._createDayHeader(null === item || void 0 === item ? void 0 : item.timestamp)
}
if (items.length - 1 === index) {
this._createMessageGroupComponent(currentMessageGroupItems, currentMessageGroupUserId)
}
}));
this._setLastMessageGroupClasses();
this._updateLoadingState(isLoading)
}
_setLastMessageGroupClasses() {
this._$content.find(".dx-chat-last-messagegroup-alignment-start").removeClass("dx-chat-last-messagegroup-alignment-start");
this._$content.find(".dx-chat-last-messagegroup-alignment-end").removeClass("dx-chat-last-messagegroup-alignment-end");
const $lastAlignmentStartGroup = this._$content.find(`.${CHAT_MESSAGEGROUP_ALIGNMENT_START_CLASS}`).last();
const $lastAlignmentEndGroup = this._$content.find(`.${CHAT_MESSAGEGROUP_ALIGNMENT_END_CLASS}`).last();
$lastAlignmentStartGroup.addClass("dx-chat-last-messagegroup-alignment-start");
$lastAlignmentEndGroup.addClass("dx-chat-last-messagegroup-alignment-end")
}
_getLastMessageGroup() {
const $lastMessageGroup = this._$content.find(`.${CHAT_MESSAGEGROUP_CLASS}`).last();
if ($lastMessageGroup.length) {
return MessageGroup.getInstance($lastMessageGroup)
}
return
}
_renderMessage(message) {
const {
timestamp: timestamp
} = message;
const shouldCreateDayHeader = this._shouldAddDayHeader(timestamp);
if (shouldCreateDayHeader) {
this._createDayHeader(timestamp);
this._renderMessageIntoGroup(message);
return
}
const lastMessageGroup = this._getLastMessageGroup();
if (!lastMessageGroup) {
this._renderMessageIntoGroup(message);
return
}
const lastMessageGroupMessage = this._getLastMessageGroupItem(lastMessageGroup);
const isTimeoutExceeded = this._isTimeoutExceeded(lastMessageGroupMessage, message);
if (this._isSameAuthor(message, lastMessageGroupMessage) && !isTimeoutExceeded) {
this._renderMessageIntoGroup(message, lastMessageGroup);
return
}
this._renderMessageIntoGroup(message)
}
_getLastMessageGroupItem(lastMessageGroup) {
const {
items: items
} = lastMessageGroup.option();
return items[items.length - 1]
}
_isSameAuthor(lastMessageGroupMessage, message) {
var _lastMessageGroupMess, _message$author;
return (null === (_lastMessageGroupMess = lastMessageGroupMessage.author) || void 0 === _lastMessageGroupMess ? void 0 : _lastMessageGroupMess.id) === (null === (_message$author = message.author) || void 0 === _message$author ? void 0 : _message$author.id)
}
_renderMessageIntoGroup(message, messageGroup) {
const {
author: author
} = message;
this._setIsReachedBottom();
if (messageGroup) {
messageGroup.renderMessage(message)
} else {
this._createMessageGroupComponent([message], null === author || void 0 === author ? void 0 : author.id);
this._setLastMessageGroupClasses()
}
this._processScrollDownContent(this._isCurrentUser(null === author || void 0 === author ? void 0 : author.id))
}
_getMessageData(message) {
return $(message).data(MESSAGE_DATA_KEY)
}
_findMessageElementByKey(key) {
const $bubbles = this.$element().find(`.${CHAT_MESSAGEBUBBLE_CLASS}`);
let result = $();
$bubbles.each(((_, item) => {
const messageData = this._getMessageData(item);
if (messageData.id === key) {
result = $(item);
return false
}
return true
}));
return result
}
_updateMessageByKey(key, data) {
if (key) {
const $targetMessage = this._findMessageElementByKey(key);
const bubble = MessageBubble.getInstance($targetMessage);
bubble.option("text", data.text)
}
}
_removeMessageByKey(key) {
if (!key) {
return
}
const $targetMessage = this._findMessageElementByKey(key);
if (!$targetMessage.length) {
return
}
const $currentMessageGroup = $targetMessage.closest(`.${CHAT_MESSAGEGROUP_CLASS}`);
const group = MessageGroup.getInstance($currentMessageGroup);
const {
items: items
} = group.option();
const newItems = items.filter((item => item.id !== key));
if (0 === newItems.length) {
const {
showDayHeaders: showDayHeaders
} = this.option();
if (showDayHeaders) {
const $prev = group.$element().prev();
const $next = group.$element().next();
const shouldRemoveDayHeader = $prev.length && $prev.hasClass("dx-chat-messagelist-day-header") && ($next.length && $next.hasClass("dx-chat-messagelist-day-header") || !$next.length);
if (shouldRemoveDayHeader) {
$prev.remove()
}
}
group.$element().remove()
} else {
group.option("items", newItems)
}
this._setLastMessageGroupClasses()
}
_scrollDownContent() {
this._scrollView.scrollTo({
top: getScrollTopMax(this._scrollableContainer())
})
}
_scrollableContainer() {
return $(this._scrollView.element()).find(".dx-scrollable-container").get(0)
}
_isMessageAddedToEnd(value, previousValue) {
const valueLength = value.length;
const previousValueLength = previousValue.length;
if (0 === valueLength) {
return false
}
if (0 === previousValueLength) {
return 1 === valueLength
}
const lastValueItem = value[valueLength - 1];
const lastPreviousValueItem = previousValue[previousValueLength - 1];
const isLastItemNotTheSame = lastValueItem !== lastPreviousValueItem;
const isLengthIncreasedByOne = valueLength - previousValueLength === 1;
return isLastItemNotTheSame && isLengthIncreasedByOne
}
_processItemsUpdating(value, previousValue) {
const shouldItemsBeUpdatedCompletely = !this._isMessageAddedToEnd(value, previousValue);
if (shouldItemsBeUpdatedCompletely) {
this._invalidate()
} else {
this._toggleEmptyView();
const newMessage = value[value.length - 1];
this._renderMessage(newMessage ?? {})
}
}
_isTimeoutExceeded(lastMessage, newMessage) {
const lastMessageTimestamp = null === lastMessage || void 0 === lastMessage ? void 0 : lastMessage.timestamp;
const newMessageTimestamp = null === newMessage || void 0 === newMessage ? void 0 : newMessage.timestamp;
if (!lastMessageTimestamp || !newMessageTimestamp) {
return false
}
const lastMessageTimestampInMs = dateSerialization.deserializeDate(lastMessageTimestamp);
const newMessageTimestampInMs = dateSerialization.deserializeDate(newMessageTimestamp);
const result = newMessageTimestampInMs - lastMessageTimestampInMs > 3e5;
return result
}
_updateAria() {
const aria = {
role: "log",
atomic: "false",
label: messageLocalization.format("dxChat-messageListAriaLabel"),
live: "polite",
relevant: "additions text"
};
this.setAria(aria)
}
_setIsReachedBottom() {
this._isBottomReached = !this._isContentOverflowing() || this._scrollView.isBottomReached()
}
_isContentOverflowing() {
return getHeight(this._scrollView.content()) > getHeight(this._scrollView.container())
}
_processScrollDownContent() {
let shouldForceProcessing = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : false;
if (this._isBottomReached || shouldForceProcessing) {
this._scrollDownContent()
}
this._isBottomReached = false
}
_$scrollViewContent() {
return $(this._scrollView.content())
}
_getEmptyView() {
return this._$content.find(".dx-chat-messagelist-empty-view")
}
_clean() {
this._lastMessageDate = null;
super._clean()
}
_modifyByChanges(changes) {
changes.forEach((change => {
switch (change.type) {
case "update":
this._updateMessageByKey(change.key, change.data ?? {});
break;
case "insert": {
const {
items: items
} = this.option();
this.option("items", [...items, change.data ?? {}]);
break
}
case "remove":
this._removeMessageByKey(change.key)
}
}))
}
_optionChanged(args) {
const {
name: name,
value: value,
previousValue: previousValue
} = args;
switch (name) {
case "currentUserId":
case "showDayHeaders":
case "showAvatar":
case "showUserName":
case "showMessageTimestamp":
case "messageTemplate":
case "dayHeaderFormat":
case "messageTimestampFormat":
this._invalidate();
break;
case "items":
this._processItemsUpdating(value ?? [], previousValue ?? []);
break;
case "typingUsers":
this._setIsReachedBottom();
this._typingIndicator.option(name, value);
this._processScrollDownContent();
break;
case "isLoading":
this._updateLoadingState(!!value);
break;
default:
super._optionChanged(args)
}
}
getEmptyViewId() {
if (this._isEmpty()) {
const $emptyView = this._getEmptyView();
const emptyViewId = $emptyView.attr("id") ?? null;
return emptyViewId
}
return null
}
}
export default MessageList;