Skip to content

Commit

Permalink
Chat - implement dayHeaderFormat and messageTimestampFormat options (#…
Browse files Browse the repository at this point in the history
…28234)

Signed-off-by: Anton Kuznetsov <[email protected]>
Co-authored-by: Anton Kuznetsov <[email protected]>
  • Loading branch information
Zedwag and ksercs authored Oct 29, 2024
1 parent cc99129 commit 55a2685
Show file tree
Hide file tree
Showing 76 changed files with 245 additions and 29 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { Format } from '@js/localization';
import messageLocalization from '@js/localization/message';
import type {
Message,
Expand All @@ -31,6 +32,8 @@ const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';

type Properties = ChatProperties & {
title: string;
dayHeaderFormat?: Format;
messageTimestampFormat?: Format;
};

class Chat extends Widget<Properties> {
Expand Down Expand Up @@ -59,6 +62,8 @@ class Chat extends Widget<Properties> {
items: [],
dataSource: null,
user: { id: new Guid().toString() },
dayHeaderFormat: 'shortdate',
messageTimestampFormat: 'shorttime',
errors: [],
onMessageSend: undefined,
onTypingStart: undefined,
Expand Down Expand Up @@ -124,7 +129,13 @@ class Chat extends Widget<Properties> {
}

_renderMessageList(): void {
const { items = [], user, showDayHeaders = true } = this.option();
const {
items = [],
user,
showDayHeaders = true,
dayHeaderFormat,
messageTimestampFormat,
} = this.option();

const currentUserId = user?.id;
const $messageList = $('<div>');
Expand All @@ -137,6 +148,8 @@ class Chat extends Widget<Properties> {
showDayHeaders,
// @ts-expect-error
isLoading: this._dataController.isLoading(),
dayHeaderFormat,
messageTimestampFormat,
});
}

Expand Down Expand Up @@ -298,6 +311,8 @@ class Chat extends Widget<Properties> {
this._createTypingEndAction();
break;
case 'showDayHeaders':
case 'dayHeaderFormat':
case 'messageTimestampFormat':
this._messageList.option(name, value);
break;
default:
Expand Down
31 changes: 23 additions & 8 deletions packages/devextreme/js/__internal/ui/chat/messagegroup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import dateSerialization from '@js/core/utils/date_serialization';
import { isDefined } from '@js/core/utils/type';
import { isDate } from '@js/core/utils/type';
import type { Format } from '@js/localization';
import dateLocalization from '@js/localization/date';
import messageLocalization from '@js/localization/message';
import type { Message } from '@js/ui/chat';
import type { WidgetOptions } from '@js/ui/widget/ui.widget';
Expand All @@ -24,6 +26,7 @@ export type MessageGroupAlignment = 'start' | 'end';
export interface Properties extends WidgetOptions<MessageGroup> {
items: Message[];
alignment: MessageGroupAlignment;
messageTimestampFormat?: Format;
}

class MessageGroup extends Widget<Properties> {
Expand All @@ -36,6 +39,7 @@ class MessageGroup extends Widget<Properties> {
...super._getDefaultOptions(),
items: [],
alignment: 'start',
messageTimestampFormat: 'shorttime',
};
}

Expand Down Expand Up @@ -131,19 +135,29 @@ class MessageGroup extends Widget<Properties> {
.addClass(CHAT_MESSAGEGROUP_TIME_CLASS)
.appendTo($information);

if (isDefined(timestamp)) {
$time.text(this._getTimeValue(timestamp));
const shouldAddTimeValue = this._shouldAddTimeValue(timestamp);

if (shouldAddTimeValue) {
const timeValue = this._getTimeValue(timestamp);
$time.text(timeValue);
}

$information.appendTo(this.element());
}

_getTimeValue(timestamp: Date | string | number): string {
const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', hour12: false };
const date = dateSerialization.deserializeDate(timestamp);
_shouldAddTimeValue(timestamp: Date | string | number | undefined): boolean {
const deserializedDate = dateSerialization.deserializeDate(timestamp);

return isDate(deserializedDate) && !isNaN(deserializedDate.getTime());
}

_getTimeValue(timestamp: Date | string | number | undefined): string {
const deserializedDate = dateSerialization.deserializeDate(timestamp);

const { messageTimestampFormat } = this.option();
const formattedTime = dateLocalization.format(deserializedDate, messageTimestampFormat);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return date.toLocaleTimeString(undefined, options);
return formattedTime as string;
}

_optionChanged(args: OptionChanged<Properties>): void {
Expand All @@ -152,6 +166,7 @@ class MessageGroup extends Widget<Properties> {
switch (name) {
case 'items':
case 'alignment':
case 'messageTimestampFormat':
this._invalidate();
break;
default:
Expand Down
19 changes: 13 additions & 6 deletions packages/devextreme/js/__internal/ui/chat/messagelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import dateUtils from '@js/core/utils/date';
import dateSerialization from '@js/core/utils/date_serialization';
import { isElementInDom } from '@js/core/utils/dom';
import { isDate, isDefined } from '@js/core/utils/type';
import type { Format } from '@js/localization';
import dateLocalization from '@js/localization/date';
import messageLocalization from '@js/localization/message';
import { getScrollTopMax } from '@js/renovation/ui/scroll_view/utils/get_scroll_top_max';
import type { Message } from '@js/ui/chat';
Expand Down Expand Up @@ -35,6 +37,8 @@ export interface Properties extends WidgetOptions<MessageList> {
items: Message[];
currentUserId: number | string | undefined;
showDayHeaders: boolean;
dayHeaderFormat?: Format;
messageTimestampFormat?: Format;
isLoading?: boolean;
}

Expand All @@ -53,6 +57,8 @@ class MessageList extends Widget<Properties> {
items: [],
currentUserId: '',
showDayHeaders: true,
dayHeaderFormat: 'shortdate',
messageTimestampFormat: 'shorttime',
isLoading: false,
};
}
Expand Down Expand Up @@ -162,10 +168,12 @@ class MessageList extends Widget<Properties> {

_createMessageGroupComponent(items: Message[], userId: string | number | undefined): void {
const $messageGroup = $('<div>').appendTo(this._$content());
const { messageTimestampFormat } = this.option();

const messageGroup = this._createComponent($messageGroup, MessageGroup, {
items,
alignment: this._messageGroupAlignment(userId),
messageTimestampFormat,
});

this._messageGroups?.push(messageGroup);
Expand Down Expand Up @@ -204,13 +212,10 @@ class MessageList extends Widget<Properties> {
const deserializedDate = dateSerialization.deserializeDate(timestamp);
const today = new Date();
const yesterday = new Date(new Date().setDate(today.getDate() - 1));
const { dayHeaderFormat } = this.option();
this._lastMessageDate = deserializedDate;

let headerDate = deserializedDate.toLocaleDateString(undefined, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).replace(/[/-]/g, '.');
let headerDate = dateLocalization.format(deserializedDate, dayHeaderFormat);

if (dateUtils.sameDate(deserializedDate, today)) {
headerDate = `${messageLocalization.format('Today')} ${headerDate}`;
Expand All @@ -222,7 +227,7 @@ class MessageList extends Widget<Properties> {

$('<div>')
.addClass(CHAT_MESSAGELIST_DAY_HEADER_CLASS)
.text(headerDate)
.text(headerDate as string)
.appendTo(this._$content());
}

Expand Down Expand Up @@ -415,6 +420,8 @@ class MessageList extends Widget<Properties> {
this._processItemsUpdating(value ?? [], previousValue ?? []);
break;
case 'showDayHeaders':
case 'dayHeaderFormat':
case 'messageTimestampFormat':
this._invalidate();
break;
case 'isLoading':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const MOCK_CHAT_HEADER_TEXT = 'Chat title';

export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID';
export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID';
export const NOW = '1721747399083';
export const NOW = 1721747399083;

export const userFirst = {
id: MOCK_COMPANION_USER_ID,
Expand Down Expand Up @@ -209,6 +209,58 @@ QUnit.module('Chat', () => {

assert.strictEqual(messageList.option('showDayHeaders'), false, 'showDayHeaders is passed on runtime');
});

QUnit.test('dayHeaderFormat option value should be passed to messageList on init', function(assert) {
const dayHeaderFormat = 'dd of MMMM, yyyy';

this.reinit({
dayHeaderFormat,
});

const messageList = this.getMessageList();

assert.strictEqual(messageList.option('dayHeaderFormat'), dayHeaderFormat, 'dayHeaderFormat is passed on init');
});

QUnit.test('messageTimestampFormat option value should be passed to messageList on init', function(assert) {
const messageTimestampFormat = 'hh hours and mm minutes';

this.reinit({
messageTimestampFormat,
});

const messageList = this.getMessageList();

assert.strictEqual(messageList.option('messageTimestampFormat'), messageTimestampFormat, 'messageTimestampFormat is passed on init');
});

QUnit.test('dayHeaderFormat option value should be passed to messageList at runtime', function(assert) {
const dayHeaderFormat = 'dd of MMMM, yyyy';

this.reinit({
dayHeaderFormat: 'yyyy',
});

this.instance.option('dayHeaderFormat', dayHeaderFormat);

const messageList = this.getMessageList();

assert.strictEqual(messageList.option('dayHeaderFormat'), dayHeaderFormat, 'dayHeaderFormat is updated at runtime');
});

QUnit.test('messageTimestampFormat option value should be passed to messageList at runtime', function(assert) {
const messageTimestampFormat = 'hh hours and mm minutes';

this.reinit({
messageTimestampFormat: 'hh',
});

this.instance.option('messageTimestampFormat', messageTimestampFormat);

const messageList = this.getMessageList();

assert.strictEqual(messageList.option('messageTimestampFormat'), messageTimestampFormat, 'messageTimestampFormat is updated at runtime');
});
});

QUnit.module('ErrorList integration', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import $ from 'jquery';

import MessageGroup from '__internal/ui/chat/messagegroup';
import ChatAvatar from '__internal/ui/chat/avatar';
import dateLocalization from 'localization/date';

const AVATAR_CLASS = 'dx-avatar';
const CHAT_MESSAGEGROUP_TIME_CLASS = 'dx-chat-messagegroup-time';
const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble';
const CHAT_MESSAGEGROUP_AUTHOR_NAME_CLASS = 'dx-chat-messagegroup-author-name';

const getStringTime = (time) => {
return dateLocalization.format(time, 'shorttime');
};

const moduleConfig = {
beforeEach: function() {
const init = (options = {}) => {
Expand Down Expand Up @@ -46,7 +51,7 @@ QUnit.module('MessageGroup', moduleConfig, () => {
const $time = this.$element.find(`.${CHAT_MESSAGEGROUP_TIME_CLASS}`);

assert.strictEqual($time.length, 1);
assert.strictEqual($time.text(), '21:34', 'time text is correct');
assert.strictEqual($time.text(), getStringTime(new Date(timestamp)), 'time text is correct');
});
});

Expand All @@ -63,7 +68,38 @@ QUnit.module('MessageGroup', moduleConfig, () => {

const $time = this.$element.find(`.${CHAT_MESSAGEGROUP_TIME_CLASS}`);

assert.strictEqual($time.text(), '21:34');
assert.strictEqual($time.text(), getStringTime(messageTimeFirst));
});

QUnit.test('time should have formatted value if messageTimestampFormat is specified on init', function(assert) {
const messageTime = new Date(2021, 9, 17, 4, 20);

this.reinit({
items: [
{ timestamp: messageTime },
],
messageTimestampFormat: 'hh_mm',
});

const $time = this.$element.find(`.${CHAT_MESSAGEGROUP_TIME_CLASS}`);

assert.strictEqual($time.text(), '04_20');
});

QUnit.test('time should have formatted value if messageTimestampFormat is specified at runtime', function(assert) {
const messageTime = new Date(2021, 9, 17, 4, 20);

this.reinit({
items: [
{ timestamp: messageTime },
],
});

this.instance.option('messageTimestampFormat', 'hh...mm');

const $time = this.$element.find(`.${CHAT_MESSAGEGROUP_TIME_CLASS}`);

assert.strictEqual($time.text(), '04...20');
});
});

Expand Down
Loading

0 comments on commit 55a2685

Please sign in to comment.