UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

566 lines (561 loc) • 24.3 kB
/** * 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;