Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat - implement dayHeaderFormat and messageTimestampFormat options #28234

Merged
merged 13 commits into from
Oct 29, 2024
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 Down Expand Up @@ -32,6 +33,8 @@ const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';
type Properties = ChatProperties & {
title: string;
showDayHeaders: boolean;
dayHeaderFormat?: Format;
messageTimestampFormat?: Format;
};

class Chat extends Widget<Properties> {
Expand Down Expand Up @@ -60,6 +63,8 @@ class Chat extends Widget<Properties> {
items: [],
dataSource: null,
user: { id: new Guid().toString() },
dayHeaderFormat: 'shortdate',
Zedwag marked this conversation as resolved.
Show resolved Hide resolved
messageTimestampFormat: 'shorttime',
errors: [],
onMessageSend: undefined,
onTypingStart: undefined,
Expand Down Expand Up @@ -125,7 +130,13 @@ class Chat extends Widget<Properties> {
}

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

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

Expand Down Expand Up @@ -299,6 +312,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
Loading