Skip to content

Commit

Permalink
Chat: Implement onTyping events
Browse files Browse the repository at this point in the history
  • Loading branch information
marker-dao authored Oct 24, 2024
1 parent 48ed0b5 commit a2ecbbb
Show file tree
Hide file tree
Showing 4 changed files with 478 additions and 13 deletions.
64 changes: 59 additions & 5 deletions packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import $ from '@js/core/renderer';
import { isDefined } from '@js/core/utils/type';
import type { Options as DataSourceOptions } from '@js/data/data_source';
import DataHelperMixin from '@js/data_helper';
import type { NativeEventInfo } from '@js/events';
import messageLocalization from '@js/localization/message';
import type {
Message,
MessageSendEvent,
Properties as ChatProperties,
User,
} from '@js/ui/chat';
import type { OptionChanged } from '@ts/core/widget/types';
import Widget from '@ts/core/widget/widget';
Expand All @@ -19,16 +21,22 @@ import ChatHeader from './header';
import type {
MessageSendEvent as MessageBoxMessageSendEvent,
Properties as MessageBoxProperties,
TypingStartEvent as MessageBoxTypingStartEvent,
} from './messagebox';
import MessageBox from './messagebox';
import MessageList from './messagelist';

const CHAT_CLASS = 'dx-chat';
const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';

type TypingStartEvent = NativeEventInfo<Chat> & { user?: User };
type TypingEndEvent = NativeEventInfo<Chat> & { user?: User };

type Properties = ChatProperties & {
title: string;
showDayHeaders: boolean;
onTypingStart?: ((e: TypingStartEvent) => void);
onTypingEnd?: ((e: TypingEndEvent) => void);
};

class Chat extends Widget<Properties> {
Expand All @@ -42,19 +50,25 @@ class Chat extends Widget<Properties> {

_messageSendAction?: (e: Partial<MessageSendEvent>) => void;

_typingStartAction?: (e: Partial<TypingStartEvent>) => void;

_typingEndAction?: (e: Partial<TypingEndEvent>) => void;

_getDefaultOptions(): Properties {
return {
...super._getDefaultOptions(),
title: '',
showDayHeaders: true,
activeStateEnabled: true,
focusStateEnabled: true,
hoverStateEnabled: true,
title: '',
items: [],
dataSource: null,
user: { id: new Guid().toString() },
onMessageSend: undefined,
showDayHeaders: true,
errors: [],
onMessageSend: undefined,
onTypingStart: undefined,
onTypingEnd: undefined,
};
}

Expand All @@ -63,11 +77,12 @@ class Chat extends Widget<Properties> {

// @ts-expect-error
this._initDataController();

// @ts-expect-error
this._refreshDataSource();

this._createMessageSendAction();
this._createTypingStartAction();
this._createTypingEndAction();
}

_dataSourceLoadErrorHandler(): void {
Expand Down Expand Up @@ -161,6 +176,12 @@ class Chat extends Widget<Properties> {
onMessageSend: (e) => {
this._messageSendHandler(e);
},
onTypingStart: (e) => {
this._typingStartHandler(e);
},
onTypingEnd: () => {
this._typingEndHandler();
},
};

this._messageBox = this._createComponent($messageBox, MessageBox, configuration);
Expand All @@ -184,7 +205,21 @@ class Chat extends Widget<Properties> {
_createMessageSendAction(): void {
this._messageSendAction = this._createActionByOption(
'onMessageSend',
{ excludeValidators: ['disabled', 'readOnly'] },
{ excludeValidators: ['disabled'] },
);
}

_createTypingStartAction(): void {
this._typingStartAction = this._createActionByOption(
'onTypingStart',
{ excludeValidators: ['disabled'] },
);
}

_createTypingEndAction(): void {
this._typingEndAction = this._createActionByOption(
'onTypingEnd',
{ excludeValidators: ['disabled'] },
);
}

Expand All @@ -201,6 +236,19 @@ class Chat extends Widget<Properties> {
this._messageSendAction?.({ message, event });
}

_typingStartHandler(e: MessageBoxTypingStartEvent): void {
const { event } = e;
const { user } = this.option();

this._typingStartAction?.({ user, event });
}

_typingEndHandler(): void {
const { user } = this.option();

this._typingEndAction?.({ user });
}

_focusTarget(): dxElementWrapper {
const $input = $(this.element()).find(`.${TEXTEDITOR_INPUT_CLASS}`);

Expand Down Expand Up @@ -249,6 +297,12 @@ class Chat extends Widget<Properties> {
case 'onMessageSend':
this._createMessageSendAction();
break;
case 'onTypingStart':
this._createTypingStartAction();
break;
case 'onTypingEnd':
this._createTypingEndAction();
break;
case 'showDayHeaders':
this._messageList.option(name, value);
break;
Expand Down
87 changes: 81 additions & 6 deletions packages/devextreme/js/__internal/ui/chat/messagebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,34 @@ import type { Properties as DOMComponentProperties } from '@ts/core/widget/dom_c
import DOMComponent from '@ts/core/widget/dom_component';
import type { OptionChanged } from '@ts/core/widget/types';

import type { EnterKeyEvent } from '../../../ui/text_area';
import type { EnterKeyEvent, InputEvent } from '../../../ui/text_area';
import type dxTextArea from '../../../ui/text_area';
import TextArea from '../m_text_area';

const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox';
const CHAT_MESSAGEBOX_TEXTAREA_CLASS = 'dx-chat-messagebox-textarea';
const CHAT_MESSAGEBOX_BUTTON_CLASS = 'dx-chat-messagebox-button';

export const TYPING_END_DELAY = 2000;

export type MessageSendEvent =
NativeEventInfo<MessageBox, KeyboardEvent | PointerEvent | MouseEvent | TouchEvent> &
{ text?: string };

export interface Properties extends DOMComponentProperties<MessageBox> {
onMessageSend?: (e: MessageSendEvent) => void;
export type TypingStartEvent = NativeEventInfo<MessageBox, UIEvent & { target: HTMLInputElement }>;

export interface Properties extends DOMComponentProperties<MessageBox> {
activeStateEnabled?: boolean;

focusStateEnabled?: boolean;

hoverStateEnabled?: boolean;

onMessageSend?: (e: MessageSendEvent) => void;

onTypingStart?: (e: TypingStartEvent) => void;

onTypingEnd?: (e: NativeEventInfo<MessageBox>) => void;
}

class MessageBox extends DOMComponent<MessageBox, Properties> {
Expand All @@ -36,20 +44,30 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {

_messageSendAction?: (e: Partial<MessageSendEvent>) => void;

_typingStartAction?: (e: Partial<TypingStartEvent>) => void;

_typingEndAction?: () => void;

_typingEndTimeoutId?: ReturnType<typeof setTimeout> | undefined;

_getDefaultOptions(): Properties {
return {
...super._getDefaultOptions(),
onMessageSend: undefined,
activeStateEnabled: true,
focusStateEnabled: true,
hoverStateEnabled: true,
onMessageSend: undefined,
onTypingStart: undefined,
onTypingEnd: undefined,
};
}

_init(): void {
super._init();

this._createMessageSendAction();
this._createTypingStartAction();
this._createTypingEndAction();
}

_initMarkup(): void {
Expand Down Expand Up @@ -81,10 +99,13 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
autoResizeEnabled: true,
valueChangeEvent: 'input',
maxHeight: '8em',
onInput: (): void => {
onInput: (e: InputEvent): void => {
const shouldButtonBeDisabled = !this._isValuableTextEntered();

this._toggleButtonDisableState(shouldButtonBeDisabled);

this._triggerTypingStartAction(e);
this._updateTypingEndTimeout();
},
onEnterKey: (e: EnterKeyEvent): void => {
if (!e.event?.shiftKey) {
Expand Down Expand Up @@ -129,15 +150,54 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
_createMessageSendAction(): void {
this._messageSendAction = this._createActionByOption(
'onMessageSend',
{ excludeValidators: ['disabled', 'readOnly'] },
{ excludeValidators: ['disabled'] },
);
}

_createTypingStartAction(): void {
this._typingStartAction = this._createActionByOption(
'onTypingStart',
{ excludeValidators: ['disabled'] },
);
}

_createTypingEndAction(): void {
this._typingEndAction = this._createActionByOption(
'onTypingEnd',
{ excludeValidators: ['disabled'] },
);
}

_triggerTypingStartAction(e: InputEvent): void {
if (!this._typingEndTimeoutId) {
this._typingStartAction?.({ event: e.event });
}
}

_updateTypingEndTimeout(): void {
clearTimeout(this._typingEndTimeoutId);

this._typingEndTimeoutId = setTimeout(() => {
this._typingEndAction?.();

this._clearTypingEndTimeout();
}, TYPING_END_DELAY);
}

_clearTypingEndTimeout(): void {
clearTimeout(this._typingEndTimeoutId);

this._typingEndTimeoutId = undefined;
}

_sendHandler(e: ClickEvent | EnterKeyEvent): void {
if (!this._isValuableTextEntered()) {
return;
}

this._clearTypingEndTimeout();
this._typingEndAction?.();

const { text } = this._textArea.option();

this._textArea.reset();
Expand Down Expand Up @@ -170,12 +230,27 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
}
case 'onMessageSend':
this._createMessageSendAction();

break;
case 'onTypingStart':
this._createTypingStartAction();

break;
case 'onTypingEnd':
this._createTypingEndAction();

break;
default:
super._optionChanged(args);
}
}

_clean(): void {
this._clearTypingEndTimeout();

super._clean();
}

updateInputAria(emptyViewId: string | null): void {
this._textArea.option({
inputAttr: {
Expand Down
Loading

0 comments on commit a2ecbbb

Please sign in to comment.