Backend API
import React, { useState, useCallback } from 'react';
import { Chat, Editing, type ChatTypes } from 'devextreme-react/chat';
import { SelectBox } from 'devextreme-react/select-box';
import { Guid } from 'devextreme-react/common';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import {
currentUser,
messages as initialMessages,
allowEditingLabel,
allowDeletingLabel,
editingOptions,
} from './data.ts';
const editingStrategy = {
enabled: true,
disabled: false,
custom: ({ component, message }) => {
const { items, user } = component.option();
const userId = user.id;
const lastNotDeletedMessage = items.findLast((item) => item.author?.id === userId && !item.isDeleted);
return message.id === lastNotDeletedMessage?.id;
},
};
const store: ChatTypes.Message[] = [...initialMessages];
const customStore = new CustomStore({
key: 'id',
load: async () => store,
insert: async (message) => {
store.push(message);
return message;
},
});
const dataSource = new DataSource({
store: customStore,
paginate: false,
});
export default function App() {
const [allowUpdating, setAllowUpdating] = useState(true);
const [allowDeleting, setAllowDeleting] = useState(true);
const onMessageEntered = useCallback((
{ message }: ChatTypes.MessageEnteredEvent,
) => {
const newMessage = {
id: new Guid().toString(),
...message,
};
dataSource.store().push([{
type: 'insert',
key: newMessage.id,
data: newMessage,
}]);
}, []);
const onMessageDeleted = useCallback((
{ message }: ChatTypes.MessageDeletedEvent,
) => {
dataSource.store().push([{
type: 'update',
key: message.id,
data: { isDeleted: true },
}]);
}, []);
const onMessageUpdated = useCallback((
{ message, text }: ChatTypes.MessageUpdatedEvent,
) => {
dataSource.store().push([{
type: 'update',
key: message.id,
data: { text, isEdited: true },
}]);
}, []);
const handleAllowUpdatingChange = useCallback((e) => {
const strategy = editingStrategy[e.value];
setAllowUpdating(() => strategy);
}, []);
const handleAllowDeletingChange = useCallback((e) => {
const strategy = editingStrategy[e.value];
setAllowDeleting(() => strategy);
}, []);
return (
<React.Fragment>
<div className="chat-container">
<Chat
height={600}
dataSource={dataSource}
user={currentUser}
reloadOnChange={false}
onMessageEntered={onMessageEntered}
onMessageDeleted={onMessageDeleted}
onMessageUpdated={onMessageUpdated}
>
<Editing
allowDeleting={allowDeleting}
allowUpdating={allowUpdating}
/>
</Chat>
</div>
<div className="options">
<div className="caption">Options</div>
<div className="option">
<span>Allow Editing:</span>
<SelectBox
items={editingOptions}
valueExpr="key"
displayExpr="text"
inputAttr= {allowEditingLabel}
defaultValue={editingOptions[0].key}
onValueChanged={handleAllowUpdatingChange}
/>
</div>
<div className="option">
<span>Allow Deleting:</span>
<SelectBox
items={editingOptions}
valueExpr="key"
displayExpr="text"
inputAttr= {allowDeletingLabel}
defaultValue={editingOptions[0].key}
onValueChanged={handleAllowDeletingChange}
/>
</div>
</div>
</React.Fragment>
);
}
import React, { useState, useCallback } from 'react';
import { Chat, Editing } from 'devextreme-react/chat';
import { SelectBox } from 'devextreme-react/select-box';
import { Guid } from 'devextreme-react/common';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import {
currentUser,
messages as initialMessages,
allowEditingLabel,
allowDeletingLabel,
editingOptions,
} from './data.js';
const editingStrategy = {
enabled: true,
disabled: false,
custom: ({ component, message }) => {
const { items, user } = component.option();
const userId = user.id;
const lastNotDeletedMessage = items.findLast(
(item) => item.author?.id === userId && !item.isDeleted,
);
return message.id === lastNotDeletedMessage?.id;
},
};
const store = [...initialMessages];
const customStore = new CustomStore({
key: 'id',
load: async() => store,
insert: async(message) => {
store.push(message);
return message;
},
});
const dataSource = new DataSource({
store: customStore,
paginate: false,
});
export default function App() {
const [allowUpdating, setAllowUpdating] = useState(true);
const [allowDeleting, setAllowDeleting] = useState(true);
const onMessageEntered = useCallback(({ message }) => {
const newMessage = {
id: new Guid().toString(),
...message,
};
dataSource.store().push([
{
type: 'insert',
key: newMessage.id,
data: newMessage,
},
]);
}, []);
const onMessageDeleted = useCallback(({ message }) => {
dataSource.store().push([
{
type: 'update',
key: message.id,
data: { isDeleted: true },
},
]);
}, []);
const onMessageUpdated = useCallback(({ message, text }) => {
dataSource.store().push([
{
type: 'update',
key: message.id,
data: { text, isEdited: true },
},
]);
}, []);
const handleAllowUpdatingChange = useCallback((e) => {
const strategy = editingStrategy[e.value];
setAllowUpdating(() => strategy);
}, []);
const handleAllowDeletingChange = useCallback((e) => {
const strategy = editingStrategy[e.value];
setAllowDeleting(() => strategy);
}, []);
return (
<React.Fragment>
<div className="chat-container">
<Chat
height={600}
dataSource={dataSource}
user={currentUser}
reloadOnChange={false}
onMessageEntered={onMessageEntered}
onMessageDeleted={onMessageDeleted}
onMessageUpdated={onMessageUpdated}
>
<Editing
allowDeleting={allowDeleting}
allowUpdating={allowUpdating}
/>
</Chat>
</div>
<div className="options">
<div className="caption">Options</div>
<div className="option">
<span>Allow Editing:</span>
<SelectBox
items={editingOptions}
valueExpr="key"
displayExpr="text"
inputAttr={allowEditingLabel}
defaultValue={editingOptions[0].key}
onValueChanged={handleAllowUpdatingChange}
/>
</div>
<div className="option">
<span>Allow Deleting:</span>
<SelectBox
items={editingOptions}
valueExpr="key"
displayExpr="text"
inputAttr={allowDeletingLabel}
defaultValue={editingOptions[0].key}
onValueChanged={handleAllowDeletingChange}
/>
</div>
</div>
</React.Fragment>
);
}
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.tsx';
ReactDOM.render(
<App />,
document.getElementById('app'),
);
import { type ChatTypes } from 'devextreme-react/chat';
import { Guid } from 'devextreme-react/common';
function getTimestamp(date, offsetMinutes = 0): number {
return date.getTime() + offsetMinutes * 60000;
}
const date = new Date();
date.setHours(0, 0, 0, 0);
export const currentUser: ChatTypes.User = {
id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3',
name: 'John Doe',
};
export const supportAgent: ChatTypes.User = {
id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3',
name: 'Support Agent',
avatarUrl: '../../../../images/petersmith.png',
};
export const messages = [
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -9),
author: supportAgent,
text: 'Hello, John!\nHow can I assist you today?',
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
text: 'Hi, I\'m having trouble accessing my account.',
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
text: 'It says my password is incorrect.',
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
isDeleted: true,
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: supportAgent,
text: 'I can help you with that. Can you please confirm your UserID for security purposes?',
isEdited: true,
},
];
export const allowEditingLabel = { 'aria-label': 'Allow Editing' };
export const allowDeletingLabel = { 'aria-label': 'Allow Deleting' };
export const editingOptions = [
{ text: 'Enabled', key: 'enabled' },
{ text: 'Disabled', key: 'disabled' },
{ text: 'Only the last message (custom)', key: 'custom' },
];
window.exports = window.exports || {};
window.config = {
transpiler: 'ts',
typescriptOptions: {
module: 'system',
emitDecoratorMetadata: true,
experimentalDecorators: true,
jsx: 'react',
},
meta: {
'react': {
'esModule': true,
},
'typescript': {
'exports': 'ts',
},
'devextreme/time_zone_utils.js': {
'esModule': true,
},
'devextreme/localization.js': {
'esModule': true,
},
'devextreme/viz/palette.js': {
'esModule': true,
},
'openai': {
'esModule': true,
},
},
paths: {
'npm:': 'https://cdn.jsdelivr.net/npm/',
'bundles:': '../../../../bundles/',
'externals:': '../../../../bundles/externals/',
},
defaultExtension: 'js',
map: {
'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js',
'typescript': 'npm:typescript@4.2.4/lib/typescript.js',
'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js',
'react': 'npm:react@17.0.2/umd/react.development.js',
'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js',
'prop-types': 'npm:prop-types/prop-types.js',
'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js',
'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js',
'es6-object-assign': 'npm:es6-object-assign',
'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs',
'devextreme-react': 'npm:devextreme-react@link:../../packages/devextreme-react/npm/cjs',
'devextreme-quill': 'npm:devextreme-quill@1.7.6/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.24/dist/dx-diagram.js',
'devexpress-gantt': 'npm:devexpress-gantt@4.1.64/dist/dx-gantt.js',
'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js',
'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js',
'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js',
'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js',
'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js',
'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js',
'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js',
'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js',
'@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js',
'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3',
// SystemJS plugins
'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js',
'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js',
// Prettier
'prettier/standalone': 'npm:prettier@2.8.8/standalone.js',
'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js',
},
packages: {
'devextreme': {
defaultExtension: 'js',
},
'devextreme-react': {
main: 'index.js',
},
'devextreme-react/common': {
main: 'index.js',
},
'devextreme/events/utils': {
main: 'index',
},
'devextreme/common/core/events/utils': {
main: 'index',
},
'devextreme/localization/messages': {
format: 'json',
defaultExtension: 'json',
},
'devextreme/events': {
main: 'index',
},
'es6-object-assign': {
main: './index.js',
defaultExtension: 'js',
},
},
packageConfigPaths: [
'npm:@devextreme/*/package.json',
],
babelOptions: {
sourceMaps: false,
stage0: true,
react: true,
},
};
System.config(window.config);
// eslint-disable-next-line
const useTgzInCSB = ['openai'];
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
ReactDOM.render(<App />, document.getElementById('app'));
import { Guid } from 'devextreme-react/common';
function getTimestamp(date, offsetMinutes = 0) {
return date.getTime() + offsetMinutes * 60000;
}
const date = new Date();
date.setHours(0, 0, 0, 0);
export const currentUser = {
id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3',
name: 'John Doe',
};
export const supportAgent = {
id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3',
name: 'Support Agent',
avatarUrl: '../../../../images/petersmith.png',
};
export const messages = [
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -9),
author: supportAgent,
text: 'Hello, John!\nHow can I assist you today?',
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
text: "Hi, I'm having trouble accessing my account.",
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
text: 'It says my password is incorrect.',
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
isDeleted: true,
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: supportAgent,
text: 'I can help you with that. Can you please confirm your UserID for security purposes?',
isEdited: true,
},
];
export const allowEditingLabel = { 'aria-label': 'Allow Editing' };
export const allowDeletingLabel = { 'aria-label': 'Allow Deleting' };
export const editingOptions = [
{ text: 'Enabled', key: 'enabled' },
{ text: 'Disabled', key: 'disabled' },
{ text: 'Only the last message (custom)', key: 'custom' },
];
<!DOCTYPE html>
<html lang="en">
<head>
<title>DevExtreme Demo</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
<link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.7/css/dx.light.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script>
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript">
System.import("./index.tsx");
</script>
</head>
<body class="dx-viewport">
<div class="demo-container">
<div id="app"></div>
</div>
</body>
</html>
#app {
min-width: 720px;
display: flex;
gap: 20px;
}
.chat-container {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
}
.options {
padding: 20px;
display: flex;
flex-direction: column;
min-width: 280px;
background-color: rgba(191, 191, 191, 0.15);
gap: 16px;
}
.dx-chat {
max-width: 480px;
}
.caption {
font-size: var(--dx-font-size-sm);
font-weight: 500;
}
.dx-avatar {
border: 1px solid var(--dx-color-border);
}
The editing object includes allowUpdating and allowDeleting properties. These Boolean options are initially set to false. To edit and delete messages, set these Boolean options to true or assign functions with custom logic.
Review this demo and learn how to delete/edit chat messages. First, ensure that "Options" are active in the panel next to the Chat component. Right-click (Control+Click on MacOS) or long-tap a message to open the context menu. Select "Delete" to remove the message; a marker is then displayed in place of the deleted message within the feed. Choose "Edit" to view the original message and update its content. Click "Send" to save changes; this will mark the message as edited.