From 691775c7c1461130823d7490ee48e614550b3038 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 15 Jan 2026 13:51:38 +0200 Subject: [PATCH] Chat: Add suggestions --- .../src/core/tokens/index.ts | 1 + .../devextreme-angular/src/ui/chat/index.ts | 67 ++++++++++++++- .../src/ui/chat/nested/index.ts | 1 + .../src/ui/chat/nested/suggestion-dxi.ts | 76 +++++++++++++++++ .../src/ui/nested/base/index.ts | 1 + .../src/ui/nested/base/suggestion-dxi.ts | 19 +++++ .../devextreme-angular/src/ui/nested/index.ts | 1 + .../src/ui/nested/suggestion-dxi.ts | 71 ++++++++++++++++ packages/devextreme-react/src/chat.ts | 27 +++++- packages/devextreme-vue/src/chat.ts | 28 +++++++ .../devextreme/js/__internal/ui/chat/chat.ts | 30 +++++++ .../ui/chat/message_box/message_box.ts | 84 ++++++++++++++++++- packages/devextreme/js/ui/chat.d.ts | 48 ++++++++++- packages/devextreme/js/ui/chat_types.d.ts | 2 + packages/devextreme/ts/dx.all.d.ts | 35 ++++++++ 15 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 packages/devextreme-angular/src/ui/chat/nested/suggestion-dxi.ts create mode 100644 packages/devextreme-angular/src/ui/nested/base/suggestion-dxi.ts create mode 100644 packages/devextreme-angular/src/ui/nested/suggestion-dxi.ts diff --git a/packages/devextreme-angular/src/core/tokens/index.ts b/packages/devextreme-angular/src/core/tokens/index.ts index b4a8073203e5..5c40f61670ba 100644 --- a/packages/devextreme-angular/src/core/tokens/index.ts +++ b/packages/devextreme-angular/src/core/tokens/index.ts @@ -18,6 +18,7 @@ export const PROPERTY_TOKEN_strips = new InjectionToken('property-token- export const PROPERTY_TOKEN_valueAxis = new InjectionToken('property-token-valueAxis'); export const PROPERTY_TOKEN_alerts = new InjectionToken('property-token-alerts'); export const PROPERTY_TOKEN_attachments = new InjectionToken('property-token-attachments'); +export const PROPERTY_TOKEN_suggestions = new InjectionToken('property-token-suggestions'); export const PROPERTY_TOKEN_typingUsers = new InjectionToken('property-token-typingUsers'); export const PROPERTY_TOKEN_ranges = new InjectionToken('property-token-ranges'); export const PROPERTY_TOKEN_groupItems = new InjectionToken('property-token-groupItems'); diff --git a/packages/devextreme-angular/src/ui/chat/index.ts b/packages/devextreme-angular/src/ui/chat/index.ts index ccd3cdcf3131..c4e95ad8b63d 100644 --- a/packages/devextreme-angular/src/ui/chat/index.ts +++ b/packages/devextreme-angular/src/ui/chat/index.ts @@ -24,7 +24,7 @@ import { import DataSource from 'devextreme/data/data_source'; import dxChat from 'devextreme/ui/chat'; -import { Alert, Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, OptionChangedEvent, TypingEndEvent, TypingStartEvent, User } from 'devextreme/ui/chat'; +import { Alert, Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, OptionChangedEvent, SuggestionClickEvent, TypingEndEvent, TypingStartEvent, Suggestion, User } from 'devextreme/ui/chat'; import { DataSourceOptions } from 'devextreme/data/data_source'; import { Store } from 'devextreme/data/store'; import { Format } from 'devextreme/common/core/localization'; @@ -50,6 +50,7 @@ import { DxoEditingModule } from 'devextreme-angular/ui/nested'; import { DxiItemModule } from 'devextreme-angular/ui/nested'; import { DxoAuthorModule } from 'devextreme-angular/ui/nested'; import { DxoMessageTimestampFormatModule } from 'devextreme-angular/ui/nested'; +import { DxiSuggestionModule } from 'devextreme-angular/ui/nested'; import { DxiTypingUserModule } from 'devextreme-angular/ui/nested'; import { DxoUserModule } from 'devextreme-angular/ui/nested'; @@ -61,12 +62,14 @@ import { DxoChatEditingModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatFileUploaderOptionsModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatItemModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatMessageTimestampFormatModule } from 'devextreme-angular/ui/chat/nested'; +import { DxiChatSuggestionModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatTypingUserModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatUserModule } from 'devextreme-angular/ui/chat/nested'; import { PROPERTY_TOKEN_alerts, PROPERTY_TOKEN_attachments, PROPERTY_TOKEN_items, + PROPERTY_TOKEN_suggestions, PROPERTY_TOKEN_typingUsers, } from 'devextreme-angular/core/tokens'; @@ -105,6 +108,11 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges this.setChildren('items', value); } + @ContentChildren(PROPERTY_TOKEN_suggestions) + set _suggestionsContentChildren(value: QueryList) { + this.setChildren('suggestions', value); + } + @ContentChildren(PROPERTY_TOKEN_typingUsers) set _typingUsersContentChildren(value: QueryList) { this.setChildren('typingUsers', value); @@ -304,6 +312,19 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges } + /** + * [descr:dxChatOptions.messageBoxValue] + + */ + @Input() + get messageBoxValue(): string { + return this._getOption('messageBoxValue'); + } + set messageBoxValue(value: string) { + this._setOption('messageBoxValue', value); + } + + /** * [descr:dxChatOptions.messageTemplate] @@ -408,6 +429,19 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges } + /** + * [descr:dxChatOptions.suggestions] + + */ + @Input() + get suggestions(): Array { + return this._getOption('suggestions'); + } + set suggestions(value: Array) { + this._setOption('suggestions', value); + } + + /** * [descr:dxChatOptions.typingUsers] @@ -547,6 +581,14 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() onOptionChanged: EventEmitter; + /** + + * [descr:dxChatOptions.onSuggestionClick] + + + */ + @Output() onSuggestionClick: EventEmitter; + /** * [descr:dxChatOptions.onTypingEnd] @@ -668,6 +710,13 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() itemsChange: EventEmitter>; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() messageBoxValueChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -724,6 +773,13 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() showUserNameChange: EventEmitter; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() suggestionsChange: EventEmitter>; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -776,6 +832,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges { subscribe: 'messageUpdated', emit: 'onMessageUpdated' }, { subscribe: 'messageUpdating', emit: 'onMessageUpdating' }, { subscribe: 'optionChanged', emit: 'onOptionChanged' }, + { subscribe: 'suggestionClick', emit: 'onSuggestionClick' }, { subscribe: 'typingEnd', emit: 'onTypingEnd' }, { subscribe: 'typingStart', emit: 'onTypingStart' }, { emit: 'accessKeyChange' }, @@ -793,6 +850,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges { emit: 'hintChange' }, { emit: 'hoverStateEnabledChange' }, { emit: 'itemsChange' }, + { emit: 'messageBoxValueChange' }, { emit: 'messageTemplateChange' }, { emit: 'messageTimestampFormatChange' }, { emit: 'reloadOnChangeChange' }, @@ -801,6 +859,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges { emit: 'showDayHeadersChange' }, { emit: 'showMessageTimestampChange' }, { emit: 'showUserNameChange' }, + { emit: 'suggestionsChange' }, { emit: 'typingUsersChange' }, { emit: 'userChange' }, { emit: 'visibleChange' }, @@ -826,6 +885,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges this.setupChanges('alerts', changes); this.setupChanges('dataSource', changes); this.setupChanges('items', changes); + this.setupChanges('suggestions', changes); this.setupChanges('typingUsers', changes); } @@ -839,6 +899,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges this._idh.doCheck('alerts'); this._idh.doCheck('dataSource'); this._idh.doCheck('items'); + this._idh.doCheck('suggestions'); this._idh.doCheck('typingUsers'); this._watcherHelper.checkWatchers(); super.ngDoCheck(); @@ -864,6 +925,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxiItemModule, DxoAuthorModule, DxoMessageTimestampFormatModule, + DxiSuggestionModule, DxiTypingUserModule, DxoUserModule, DxiChatAlertModule, @@ -874,6 +936,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatFileUploaderOptionsModule, DxiChatItemModule, DxoChatMessageTimestampFormatModule, + DxiChatSuggestionModule, DxiChatTypingUserModule, DxoChatUserModule, DxIntegrationModule, @@ -887,6 +950,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxiItemModule, DxoAuthorModule, DxoMessageTimestampFormatModule, + DxiSuggestionModule, DxiTypingUserModule, DxoUserModule, DxiChatAlertModule, @@ -897,6 +961,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatFileUploaderOptionsModule, DxiChatItemModule, DxoChatMessageTimestampFormatModule, + DxiChatSuggestionModule, DxiChatTypingUserModule, DxoChatUserModule, DxTemplateModule diff --git a/packages/devextreme-angular/src/ui/chat/nested/index.ts b/packages/devextreme-angular/src/ui/chat/nested/index.ts index 93eac66e115c..28274d7f0b7e 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/index.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/index.ts @@ -6,6 +6,7 @@ export * from './editing'; export * from './file-uploader-options'; export * from './item-dxi'; export * from './message-timestamp-format'; +export * from './suggestion-dxi'; export * from './typing-user-dxi'; export * from './user'; diff --git a/packages/devextreme-angular/src/ui/chat/nested/suggestion-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/suggestion-dxi.ts new file mode 100644 index 000000000000..0140c6ee21d2 --- /dev/null +++ b/packages/devextreme-angular/src/ui/chat/nested/suggestion-dxi.ts @@ -0,0 +1,76 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + +import { PROPERTY_TOKEN_suggestions } from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxi-chat-suggestion', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [ + NestedOptionHost, + { + provide: PROPERTY_TOKEN_suggestions, + useExisting: DxiChatSuggestionComponent, + } + ] +}) +export class DxiChatSuggestionComponent extends CollectionNestedOption { + @Input() + get text(): string { + return this._getOption('text'); + } + set text(value: string) { + this._setOption('text', value); + } + + + protected get _optionPath() { + return 'suggestions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + imports: [ + DxiChatSuggestionComponent + ], + exports: [ + DxiChatSuggestionComponent + ], +}) +export class DxiChatSuggestionModule { } diff --git a/packages/devextreme-angular/src/ui/nested/base/index.ts b/packages/devextreme-angular/src/ui/nested/base/index.ts index de74c6df0cfc..865e593d00cc 100644 --- a/packages/devextreme-angular/src/ui/nested/base/index.ts +++ b/packages/devextreme-angular/src/ui/nested/base/index.ts @@ -50,6 +50,7 @@ export * from './search-panel'; export * from './sortable-options'; export * from './sorting'; export * from './splitter-options'; +export * from './suggestion-dxi'; export * from './tab-panel-options'; export * from './text-box-options'; export * from './text-editor-button-dxi'; diff --git a/packages/devextreme-angular/src/ui/nested/base/suggestion-dxi.ts b/packages/devextreme-angular/src/ui/nested/base/suggestion-dxi.ts new file mode 100644 index 000000000000..69ea47edcf6f --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/suggestion-dxi.ts @@ -0,0 +1,19 @@ +/* tslint:disable:max-line-length */ + +import { CollectionNestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxiSuggestion extends CollectionNestedOption { + get text(): string { + return this._getOption('text'); + } + set text(value: string) { + this._setOption('text', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/index.ts b/packages/devextreme-angular/src/ui/nested/index.ts index 7c291f7dc04e..16b2ef3c28a0 100644 --- a/packages/devextreme-angular/src/ui/nested/index.ts +++ b/packages/devextreme-angular/src/ui/nested/index.ts @@ -211,6 +211,7 @@ export * from './strip-line-dxi'; export * from './strip-style'; export * from './subtitle'; export * from './subvalue-indicator'; +export * from './suggestion-dxi'; export * from './summary'; export * from './tab-dxi'; export * from './tab-panel-options'; diff --git a/packages/devextreme-angular/src/ui/nested/suggestion-dxi.ts b/packages/devextreme-angular/src/ui/nested/suggestion-dxi.ts new file mode 100644 index 000000000000..652e7d541e24 --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/suggestion-dxi.ts @@ -0,0 +1,71 @@ +/* tslint:disable:max-line-length */ + +/* tslint:disable:use-input-property-decorator */ + +import { + Component, + NgModule, + Host, + SkipSelf +} from '@angular/core'; + + + + + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { DxiSuggestion } from './base/suggestion-dxi'; + +import { PROPERTY_TOKEN_suggestions } from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxi-suggestion', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [ + NestedOptionHost, + { + provide: PROPERTY_TOKEN_suggestions, + useExisting: DxiSuggestionComponent, + } + ], + inputs: [ + 'text' + ] +}) +export class DxiSuggestionComponent extends DxiSuggestion { + + protected get _optionPath() { + return 'suggestions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + imports: [ + DxiSuggestionComponent + ], + exports: [ + DxiSuggestionComponent + ], +}) +export class DxiSuggestionModule { } diff --git a/packages/devextreme-react/src/chat.ts b/packages/devextreme-react/src/chat.ts index c427f7135b74..e815ad98c9a9 100644 --- a/packages/devextreme-react/src/chat.ts +++ b/packages/devextreme-react/src/chat.ts @@ -8,7 +8,7 @@ import dxChat, { import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component"; import NestedOption from "./core/nested-option"; -import type { Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, TypingEndEvent, TypingStartEvent, Attachment as ChatAttachment, User as ChatUser } from "devextreme/ui/chat"; +import type { Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, SuggestionClickEvent, TypingEndEvent, TypingStartEvent, Attachment as ChatAttachment, User as ChatUser } from "devextreme/ui/chat"; import type { DisposingEvent as FileUploaderDisposingEvent, InitializedEvent as FileUploaderInitializedEvent, BeforeSendEvent, ContentReadyEvent, DropZoneEnterEvent, DropZoneLeaveEvent, FilesUploadedEvent, OptionChangedEvent, ProgressEvent, UploadAbortedEvent, UploadedEvent, UploadErrorEvent, UploadStartedEvent, ValueChangedEvent, UploadHttpMethod, FileUploadMode } from "devextreme/ui/file_uploader"; import type { Format, ValidationStatus } from "devextreme/common"; @@ -29,6 +29,7 @@ type IChatOptionsNarrowedEvents = { onMessageEntered?: ((e: MessageEnteredEvent) => void) | undefined; onMessageUpdated?: ((e: MessageUpdatedEvent) => void) | undefined; onMessageUpdating?: ((e: MessageUpdatingEvent) => void) | undefined; + onSuggestionClick?: ((e: SuggestionClickEvent) => void) | undefined; onTypingEnd?: ((e: TypingEndEvent) => void) | undefined; onTypingStart?: ((e: TypingStartEvent) => void) | undefined; } @@ -60,7 +61,7 @@ const Chat = memo( ), []); const subscribableOptions = useMemo(() => (["items"]), []); - const independentEvents = useMemo(() => (["onAttachmentDownloadClick","onDisposing","onInitialized","onMessageDeleted","onMessageDeleting","onMessageEditCanceled","onMessageEditingStart","onMessageEntered","onMessageUpdated","onMessageUpdating","onTypingEnd","onTypingStart"]), []); + const independentEvents = useMemo(() => (["onAttachmentDownloadClick","onDisposing","onInitialized","onMessageDeleted","onMessageDeleting","onMessageEditCanceled","onMessageEditingStart","onMessageEntered","onMessageUpdated","onMessageUpdating","onSuggestionClick","onTypingEnd","onTypingStart"]), []); const defaults = useMemo(() => ({ defaultItems: "items", @@ -73,6 +74,7 @@ const Chat = memo( fileUploaderOptions: { optionName: "fileUploaderOptions", isCollectionItem: false }, item: { optionName: "items", isCollectionItem: true }, messageTimestampFormat: { optionName: "messageTimestampFormat", isCollectionItem: false }, + suggestion: { optionName: "suggestions", isCollectionItem: true }, typingUser: { optionName: "typingUsers", isCollectionItem: true }, user: { optionName: "user", isCollectionItem: false } }), []); @@ -351,6 +353,25 @@ const MessageTimestampFormat = Object.assign +const _componentSuggestion = (props: ISuggestionProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "suggestions", + IsCollectionItem: true, + }, + }); +}; + +const Suggestion = Object.assign(_componentSuggestion, { + componentType: "option", +}); + // owners: // Chat type ITypingUserProps = React.PropsWithChildren<{ @@ -415,6 +436,8 @@ export { IItemProps, MessageTimestampFormat, IMessageTimestampFormatProps, + Suggestion, + ISuggestionProps, TypingUser, ITypingUserProps, User, diff --git a/packages/devextreme-vue/src/chat.ts b/packages/devextreme-vue/src/chat.ts index ff0edbe3dcd7..8ce30c80a609 100644 --- a/packages/devextreme-vue/src/chat.ts +++ b/packages/devextreme-vue/src/chat.ts @@ -19,8 +19,10 @@ import { MessageUpdatedEvent, MessageUpdatingEvent, OptionChangedEvent, + SuggestionClickEvent, TypingEndEvent, TypingStartEvent, + Suggestion, User, Attachment, } from "devextreme/ui/chat"; @@ -87,6 +89,7 @@ type AccessibleOptions = Pick void)>, onMessageUpdating: Function as PropType<((e: MessageUpdatingEvent) => void)>, onOptionChanged: Function as PropType<((e: OptionChangedEvent) => void)>, + onSuggestionClick: Function as PropType<((e: SuggestionClickEvent) => void)>, onTypingEnd: Function as PropType<((e: TypingEndEvent) => void)>, onTypingStart: Function as PropType<((e: TypingStartEvent) => void)>, reloadOnChange: Boolean, @@ -143,6 +148,7 @@ const componentConfig = { showDayHeaders: Boolean, showMessageTimestamp: Boolean, showUserName: Boolean, + suggestions: Array as PropType>, typingUsers: Array as PropType>, user: Object as PropType>, visible: Boolean, @@ -179,6 +185,7 @@ const componentConfig = { "update:onMessageUpdated": null, "update:onMessageUpdating": null, "update:onOptionChanged": null, + "update:onSuggestionClick": null, "update:onTypingEnd": null, "update:onTypingStart": null, "update:reloadOnChange": null, @@ -187,6 +194,7 @@ const componentConfig = { "update:showDayHeaders": null, "update:showMessageTimestamp": null, "update:showUserName": null, + "update:suggestions": null, "update:typingUsers": null, "update:user": null, "update:visible": null, @@ -207,6 +215,7 @@ const componentConfig = { fileUploaderOptions: { isCollectionItem: false, optionName: "fileUploaderOptions" }, item: { isCollectionItem: true, optionName: "items" }, messageTimestampFormat: { isCollectionItem: false, optionName: "messageTimestampFormat" }, + suggestion: { isCollectionItem: true, optionName: "suggestions" }, typingUser: { isCollectionItem: true, optionName: "typingUsers" }, user: { isCollectionItem: false, optionName: "user" } }; @@ -537,6 +546,24 @@ const DxMessageTimestampFormat = defineComponent(DxMessageTimestampFormatConfig) (DxMessageTimestampFormat as any).$_optionName = "messageTimestampFormat"; +const DxSuggestionConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:text": null, + }, + props: { + text: String + } +}; + +prepareConfigurationComponentConfig(DxSuggestionConfig); + +const DxSuggestion = defineComponent(DxSuggestionConfig); + +(DxSuggestion as any).$_optionName = "suggestions"; +(DxSuggestion as any).$_isCollectionItem = true; + const DxTypingUserConfig = { emits: { "update:isActive": null, @@ -595,6 +622,7 @@ export { DxFileUploaderOptions, DxItem, DxMessageTimestampFormat, + DxSuggestion, DxTypingUser, DxUser }; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 79bcf861a19c..3e075a332fcf 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -17,6 +17,8 @@ import type { MessageUpdatedEvent, MessageUpdatingEvent, Properties, + Suggestion, + SuggestionClickEvent, TypingEndEvent, TypingStartEvent, } from '@js/ui/chat'; @@ -76,6 +78,8 @@ class Chat extends Widget { _attachmentDownloadAction?: (e: Partial) => void; + _suggestionClickAction?: (e: Partial) => void; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -130,6 +134,7 @@ class Chat extends Widget { this._createTypingStartAction(); this._createTypingEndAction(); this._createAttachmentDownloadAction(); + this._createSuggestionClickAction(); } _dataSourceLoadErrorHandler(): void { @@ -442,6 +447,10 @@ class Chat extends Widget { ); } + _suggestionClickHandler(e: { suggestion: Suggestion }): void { + this._suggestionClickAction?.(e); + } + _renderAlertList(): void { const $errors = $('
'); @@ -460,6 +469,7 @@ class Chat extends Widget { fileUploaderOptions, focusStateEnabled, hoverStateEnabled, + suggestions, } = this.option(); const $messageBox = $('
'); @@ -471,6 +481,7 @@ class Chat extends Widget { fileUploaderOptions, focusStateEnabled, hoverStateEnabled, + suggestions, onMessageEntered: (e) => { this._messageEnteredHandler(e); }, @@ -486,6 +497,9 @@ class Chat extends Widget { onMessageUpdating: (e) => { this._messageUpdatingHandler(e); }, + onSuggestionClick: (e) => { + this._suggestionClickHandler(e); + }, }; this._messageBox = this._createComponent($messageBox, MessageBox, configuration); @@ -576,6 +590,13 @@ class Chat extends Widget { ); } + _createSuggestionClickAction(): void { + this._suggestionClickAction = this._createActionByOption( + 'onSuggestionClick', + { excludeValidators: ['disabled'] }, + ); + } + _messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void { const { text, event, attachments } = e; const { user } = this.option(); @@ -656,6 +677,12 @@ class Chat extends Widget { case 'alerts': this._alertList.option('items', value ?? []); break; + case 'suggestions': + this._messageBox.option('suggestions', value); + break; + case 'messageBoxValue': + this._messageBox.option('textAreaValue', value); + break; case 'onMessageEntered': this._createMessageEnteredAction(); break; @@ -687,6 +714,9 @@ class Chat extends Widget { this._createAttachmentDownloadAction(); this._updateAttachmentDownloadHandler(); break; + case 'onSuggestionClick': + this._createSuggestionClickAction(); + break; case 'showDayHeaders': case 'showAvatar': case 'showUserName': diff --git a/packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts b/packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts index f528492200a7..740d5516c44a 100644 --- a/packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts +++ b/packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts @@ -1,12 +1,14 @@ import type { NativeEventInfo } from '@js/common/core/events'; import $, { type dxElementWrapper } from '@js/core/renderer'; import type { InteractionEvent } from '@js/events'; -import type { Attachment } from '@js/ui/chat'; +import type { Properties as ButtonGroupProperties } from '@js/ui/button_group'; +import type { Attachment, Suggestion } from '@js/ui/chat'; import type { Properties as FileUploaderProperties } from '@js/ui/file_uploader'; import type { InputEvent } from '@js/ui/text_area'; import type { DOMComponentProperties } from '@ts/core/widget/dom_component'; import DOMComponent from '@ts/core/widget/dom_component'; import type { OptionChanged } from '@ts/core/widget/types'; +import ButtonGroup from '@ts/ui/button_group'; import type { Properties as ChatTextAreaProperties, SendEvent, @@ -16,6 +18,7 @@ import EditingPreview from '@ts/ui/chat/message_box/editing_preview'; export const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox'; export const CHAT_MESSAGEBOX_TEXTAREA_CONTAINER_CLASS = 'dx-chat-messagebox-textarea-container'; +export const CHAT_MESSAGEBOX_SUGGESTIONS_CLASS = 'dx-chat-messagebox-suggestions'; export const TYPING_END_DELAY = 2000; const ESCAPE_KEY = 'escape'; @@ -40,6 +43,10 @@ export interface Properties extends DOMComponentProperties { text?: string; + suggestions?: Suggestion[]; + + textAreaValue?: string; + onMessageEntered?: (e: MessageEnteredEvent) => void; onTypingStart?: (e: TypingStartEvent) => void; @@ -49,6 +56,8 @@ export interface Properties extends DOMComponentProperties { onMessageEditCanceled?: () => void; onMessageUpdating?: (e: { text: string }) => void; + + onSuggestionClick?: (e: { suggestion: Suggestion }) => void; } class MessageBox extends DOMComponent { @@ -56,6 +65,8 @@ class MessageBox extends DOMComponent { _editingPreview!: EditingPreview | null; + _suggestionsButtonGroup?: ButtonGroup; + _messageEnteredAction?: (e: Partial) => void; _typingStartAction?: (e: Partial) => void; @@ -73,11 +84,13 @@ class MessageBox extends DOMComponent { hoverStateEnabled: true, fileUploaderOptions: undefined, text: '', + suggestions: [], onMessageEntered: undefined, onMessageEditCanceled: undefined, onMessageUpdating: undefined, onTypingStart: undefined, onTypingEnd: undefined, + onSuggestionClick: undefined, }; } @@ -98,6 +111,8 @@ class MessageBox extends DOMComponent { this._renderEditingPreview(); } + this._renderSuggestions(); + this._renderTextAreaContainer(); } @@ -154,6 +169,23 @@ class MessageBox extends DOMComponent { }); } + _renderSuggestions(): void { + const { suggestions } = this.option(); + if (!suggestions?.length) { + return; + } + + const $buttonGroup = $('
') + .addClass(CHAT_MESSAGEBOX_SUGGESTIONS_CLASS) + .appendTo(this.element()); + + this._suggestionsButtonGroup = this._createComponent( + $buttonGroup, + ButtonGroup, + this._getSuggestionsButtonGroupOptions(suggestions), + ); + } + _getTextAreaOptions(): ChatTextAreaProperties { const { activeStateEnabled, @@ -181,6 +213,47 @@ class MessageBox extends DOMComponent { return options as ChatTextAreaProperties; } + _removeSuggestions(): void { + this._suggestionsButtonGroup?.dispose(); + this._suggestionsButtonGroup = undefined; + } + + _handleSuggestionsOptionChanged(): void { + const { suggestions } = this.option(); + + if (this._suggestionsButtonGroup) { + if (suggestions?.length) { + this._suggestionsButtonGroup.option('items', suggestions); + } else { + this._removeSuggestions(); + } + } else if (suggestions?.length) { + this._renderSuggestions(); + } + } + + _getSuggestionsButtonGroupOptions(suggestions: Suggestion[]): ButtonGroupProperties { + const { + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + onSuggestionClick, + } = this.option(); + + return { + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + items: suggestions, + displayExpr: 'text', + selectionMode: 'none', + stylingMode: 'outlined', + onItemClick: (e: { itemData: Suggestion }) => { + onSuggestionClick?.({ suggestion: e.itemData }); + }, + } as ButtonGroupProperties; + } + _createMessageEnteredAction(): void { this._messageEnteredAction = this._createActionByOption( 'onMessageEntered', @@ -285,6 +358,14 @@ class MessageBox extends DOMComponent { this._textArea.option('value', value); break; + case 'suggestions': + this._handleSuggestionsOptionChanged(); + break; + + case 'textAreaValue': + this._textArea.option('value', value); + break; + default: super._optionChanged(args); } @@ -292,6 +373,7 @@ class MessageBox extends DOMComponent { _clean(): void { this._clearTypingEndTimeout(); + this._removeSuggestions(); super._clean(); } diff --git a/packages/devextreme/js/ui/chat.d.ts b/packages/devextreme/js/ui/chat.d.ts index b2e64cf94ca5..84ad95f826e4 100644 --- a/packages/devextreme/js/ui/chat.d.ts +++ b/packages/devextreme/js/ui/chat.d.ts @@ -165,6 +165,17 @@ export type AttachmentDownloadClickEvent = EventInfo & { readonly attachment?: Attachment; }; +/** + * @docid _ui_chat_SuggestionClickEvent + * @public + * @type object + * @inherits NativeEventInfo + */ +export type SuggestionClickEvent = NativeEventInfo & { + /** @docid _ui_chat_SuggestionClickEvent.suggestion */ + readonly suggestion?: Suggestion; +}; + /** * @docid * @namespace DevExpress.ui.dxChat @@ -231,6 +242,21 @@ export type Attachment = { [key: string]: any; }; +/** + * @docid + * @namespace DevExpress.ui.dxChat + * @public + */ +export type Suggestion = { + /** + * @docid + * @public + */ + text: string; + + [key: string]: any; +}; + /** * @docid * @namespace DevExpress.ui.dxChat @@ -432,6 +458,12 @@ export interface dxChatOptions extends WidgetOptions { * @public */ alerts?: Array; + /** + * @docid + * @default '' + * @public + */ + messageBoxValue?: string; /** * @docid * @default null @@ -475,6 +507,12 @@ export interface dxChatOptions extends WidgetOptions { * @public */ showMessageTimestamp?: boolean; + /** + * @docid + * @public + * @type Array + */ + suggestions?: Suggestion[]; /** * @docid * @default undefined @@ -555,6 +593,14 @@ export interface dxChatOptions extends WidgetOptions { * @public */ onMessageUpdated?: ((e: MessageUpdatedEvent) => void) | undefined; + /** + * @docid + * @default undefined + * @type_function_param1 e:{ui/chat:SuggestionClickEvent} + * @action + * @public + */ + onSuggestionClick?: ((e: SuggestionClickEvent) => void) | undefined; } /** @@ -593,7 +639,7 @@ type FilterOutHidden = Omit, Required, 'onMessageEntered' | 'onTypingStart' | 'onTypingEnd' | 'onMessageDeleting' | 'onMessageDeleted' - | 'onMessageEditingStart' | 'onMessageEditCanceled' | 'onMessageUpdating' | 'onMessageUpdated' | 'onAttachmentDownloadClick'>; + | 'onMessageEditingStart' | 'onMessageEditCanceled' | 'onMessageUpdating' | 'onMessageUpdated' | 'onAttachmentDownloadClick' | 'onSuggestionClick'>; /** * @hidden diff --git a/packages/devextreme/js/ui/chat_types.d.ts b/packages/devextreme/js/ui/chat_types.d.ts index 993170fe0bc4..e8e91c0b778b 100644 --- a/packages/devextreme/js/ui/chat_types.d.ts +++ b/packages/devextreme/js/ui/chat_types.d.ts @@ -12,9 +12,11 @@ export { MessageUpdatingEvent, MessageUpdatedEvent, AttachmentDownloadClickEvent, + SuggestionClickEvent, User, Alert, Attachment, + Suggestion, TextMessage, ImageMessage, Message, diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 4a7bc64f3a1c..e024da47f767 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -11304,6 +11304,16 @@ declare module DevExpress.ui { DevExpress.common.core.events.EventInfo & DevExpress.common.core.events.ChangedOptionInfo; export type Properties = dxChatOptions; + /** + * [descr:_ui_chat_SuggestionClickEvent] + */ + export type SuggestionClickEvent = + DevExpress.common.core.events.NativeEventInfo & { + /** + * [descr:_ui_chat_SuggestionClickEvent.suggestion] + */ + readonly suggestion?: Suggestion; + }; /** * [descr:_ui_chat_TypingEndEvent] */ @@ -11409,6 +11419,10 @@ declare module DevExpress.ui { * [descr:dxChatOptions.alerts] */ alerts?: Array; + /** + * [descr:dxChatOptions.messageBoxValue] + */ + messageBoxValue?: string; /** * [descr:dxChatOptions.messageTemplate] */ @@ -11443,6 +11457,10 @@ declare module DevExpress.ui { * [descr:dxChatOptions.showMessageTimestamp] */ showMessageTimestamp?: boolean; + /** + * [descr:dxChatOptions.suggestions] + */ + suggestions?: DevExpress.ui.dxChat.Suggestion[]; /** * [descr:dxChatOptions.onAttachmentDownloadClick] */ @@ -11503,6 +11521,12 @@ declare module DevExpress.ui { onMessageUpdated?: | ((e: DevExpress.ui.dxChat.MessageUpdatedEvent) => void) | undefined; + /** + * [descr:dxChatOptions.onSuggestionClick] + */ + onSuggestionClick?: + | ((e: DevExpress.ui.dxChat.SuggestionClickEvent) => void) + | undefined; } /** * [descr:dxCheckBox] @@ -34037,6 +34061,17 @@ declare module DevExpress.ui.dxChat { [key: string]: any; }; + /** + * [descr:Suggestion] + */ + export type Suggestion = { + /** + * [descr:Suggestion.text] + */ + text: string; + + [key: string]: any; + }; /** * [descr:TextMessage] */