devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
566 lines (561 loc) • 24.3 kB
JavaScript
/**
* DevExtreme (cjs/__internal/ui/chat/messagelist.js)
* Version: 24.2.6
* Build date: Mon Mar 17 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.MESSAGEGROUP_TIMEOUT = void 0;
var _common = require("../../../common");
var _date = _interopRequireDefault(require("../../../common/core/localization/date"));
var _message = _interopRequireDefault(require("../../../common/core/localization/message"));
var _renderer = _interopRequireDefault(require("../../../core/renderer"));
var _resize_observer = _interopRequireDefault(require("../../../core/resize_observer"));
var _common2 = require("../../../core/utils/common");
var _date2 = _interopRequireDefault(require("../../../core/utils/date"));
var _date_serialization = _interopRequireDefault(require("../../../core/utils/date_serialization"));
var _dom = require("../../../core/utils/dom");
var _size = require("../../../core/utils/size");
var _type = require("../../../core/utils/type");
var _scroll_view = _interopRequireDefault(require("../../../ui/scroll_view"));
var _widget = _interopRequireDefault(require("../../core/widget/widget"));
var _get_scroll_top_max = require("../../ui/scroll_view/utils/get_scroll_top_max");
var _layout = require("../splitter/utils/layout");
var _messagebubble = _interopRequireWildcard(require("./messagebubble"));
var _messagegroup = _interopRequireWildcard(require("./messagegroup"));
var _typingindicator = _interopRequireDefault(require("./typingindicator"));
function _getRequireWildcardCache(e) {
if ("function" != typeof WeakMap) {
return null
}
var r = new WeakMap,
t = new WeakMap;
return (_getRequireWildcardCache = function(e) {
return e ? t : r
})(e)
}
function _interopRequireWildcard(e, r) {
if (!r && e && e.__esModule) {
return e
}
if (null === e || "object" != typeof e && "function" != typeof e) {
return {
default: e
}
}
var t = _getRequireWildcardCache(r);
if (t && t.has(e)) {
return t.get(e)
}
var n = {
__proto__: null
},
a = Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var u in e) {
if ("default" !== u && {}.hasOwnProperty.call(e, u)) {
var i = a ? Object.getOwnPropertyDescriptor(e, u) : null;
i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]
}
}
return n.default = e, t && t.set(e, n), n
}
function _interopRequireDefault(e) {
return e && e.__esModule ? e : {
default: e
}
}
function _extends() {
return _extends = Object.assign ? Object.assign.bind() : function(n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) {
({}).hasOwnProperty.call(t, r) && (n[r] = t[r])
}
}
return n
}, _extends.apply(null, arguments)
}
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";
const MESSAGEGROUP_TIMEOUT = exports.MESSAGEGROUP_TIMEOUT = 3e5;
class MessageList extends _widget.default {
_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() {
(0, _renderer.default)(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);
_resize_observer.default.unobserve(element);
_resize_observer.default.observe(element, (entry => this._resizeHandler(entry)))
}
_resizeHandler(_ref) {
let {
contentRect: contentRect,
target: target
} = _ref;
if (!(0, _dom.isElementInDom)((0, _renderer.default)(target)) || !(0, _layout.isElementVisible)(target)) {
return
}
const isInitialRendering = !(0, _type.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 = (0, _renderer.default)("<div>").addClass("dx-chat-messagelist-empty-view").attr("id", `dx-${new _common.Guid}`);
(0, _renderer.default)("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-image");
const messageText = _message.default.format("dxChat-emptyListMessage");
(0, _renderer.default)("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-message").text(messageText);
const promptText = _message.default.format("dxChat-emptyListPrompt");
(0, _renderer.default)("<div>").appendTo($emptyView).addClass("dx-chat-messagelist-empty-prompt").text(promptText);
$emptyView.appendTo(this._$content)
}
_renderTypingIndicator() {
const {
typingUsers: typingUsers
} = this.option();
const $typingIndicator = (0, _renderer.default)("<div>").appendTo(this._$scrollViewContent());
this._typingIndicator = this._createComponent($typingIndicator, _typingindicator.default, {
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 = (0, _renderer.default)("<div>").appendTo(this._$content);
this._createComponent($messageGroup, _messagegroup.default, {
items: items,
alignment: this._messageGroupAlignment(userId),
showAvatar: showAvatar,
showUserName: showUserName,
showMessageTimestamp: showMessageTimestamp,
messageTemplate: messageTemplate,
messageTimestampFormat: messageTimestampFormat
})
}
_renderScrollView() {
const $scrollable = (0, _renderer.default)("<div>").appendTo(this.$element());
this._scrollView = this._createComponent($scrollable, _scroll_view.default, {
useKeyboard: false,
bounceEnabled: false,
reachBottomText: "",
indicateLoading: false,
onReachBottom: _common2.noop
})
}
_shouldAddDayHeader(timestamp) {
const {
showDayHeaders: showDayHeaders
} = this.option();
if (!showDayHeaders) {
return false
}
const deserializedDate = _date_serialization.default.deserializeDate(timestamp);
if (!(0, _type.isDate)(deserializedDate) || isNaN(deserializedDate.getTime())) {
return false
}
return !_date2.default.sameDate(this._lastMessageDate, deserializedDate)
}
_createDayHeader(timestamp) {
const deserializedDate = _date_serialization.default.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 = _date.default.format(deserializedDate, dayHeaderFormat);
if (_date2.default.sameDate(deserializedDate, today)) {
headerDate = `${_message.default.format("Today")} ${headerDate}`
}
if (_date2.default.sameDate(deserializedDate, yesterday)) {
headerDate = `${_message.default.format("Yesterday")} ${headerDate}`
}(0, _renderer.default)("<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 = (0, _renderer.default)("<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(`.${_messagegroup.CHAT_MESSAGEGROUP_ALIGNMENT_START_CLASS}`).last();
const $lastAlignmentEndGroup = this._$content.find(`.${_messagegroup.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(`.${_messagegroup.CHAT_MESSAGEGROUP_CLASS}`).last();
if ($lastMessageGroup.length) {
return _messagegroup.default.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 (0, _renderer.default)(message).data(_messagegroup.MESSAGE_DATA_KEY)
}
_findMessageElementByKey(key) {
const $bubbles = this.$element().find(`.${_messagebubble.CHAT_MESSAGEBUBBLE_CLASS}`);
let result = (0, _renderer.default)();
$bubbles.each(((_, item) => {
const messageData = this._getMessageData(item);
if (messageData.id === key) {
result = (0, _renderer.default)(item);
return false
}
return true
}));
return result
}
_updateMessageByKey(key, data) {
if (key) {
const $targetMessage = this._findMessageElementByKey(key);
const bubble = _messagebubble.default.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(`.${_messagegroup.CHAT_MESSAGEGROUP_CLASS}`);
const group = _messagegroup.default.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: (0, _get_scroll_top_max.getScrollTopMax)(this._scrollableContainer())
})
}
_scrollableContainer() {
return (0, _renderer.default)(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 = _date_serialization.default.deserializeDate(lastMessageTimestamp);
const newMessageTimestampInMs = _date_serialization.default.deserializeDate(newMessageTimestamp);
const result = newMessageTimestampInMs - lastMessageTimestampInMs > MESSAGEGROUP_TIMEOUT;
return result
}
_updateAria() {
const aria = {
role: "log",
atomic: "false",
label: _message.default.format("dxChat-messageListAriaLabel"),
live: "polite",
relevant: "additions text"
};
this.setAria(aria)
}
_setIsReachedBottom() {
this._isBottomReached = !this._isContentOverflowing() || this._scrollView.isBottomReached()
}
_isContentOverflowing() {
return (0, _size.getHeight)(this._scrollView.content()) > (0, _size.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 (0, _renderer.default)(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
}
}
var _default = exports.default = MessageList;