Backend API
import React, { useCallback, useRef } from 'react';
import Chat, { FileUploaderOptions } from 'devextreme-react/chat';
import type { ChatTypes } from 'devextreme-react/chat';
import type { FileUploaderTypes } from 'devextreme-react/file-uploader';
import { Guid } from 'devextreme-react/common';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import { currentUser, messages as initialMessages } from './data.ts';
const store: ChatTypes.Message[] = [...initialMessages];
const customStore = new CustomStore({
key: 'id',
load: () => Promise.resolve(store),
insert: (message: ChatTypes.Message) => {
store.push(message);
return Promise.resolve(message);
},
});
const dataSource = new DataSource({
store: customStore,
paginate: false,
});
export default function App() {
const uploadedFilesMapRef = useRef<Map<string, string>>(new Map<string, string>());
function getFileUrl(filename: string): string | undefined {
return uploadedFilesMapRef.current.get(filename);
}
const onUploaded = useCallback((e: FileUploaderTypes.UploadedEvent): void => {
const { file } = e;
const url = URL.createObjectURL(file);
uploadedFilesMapRef.current.set(file.name, url);
}, []);
const onMessageEntered = useCallback((
{ message }: ChatTypes.MessageEnteredEvent,
): void => {
const attachmentsWithUrls = message.attachments?.map((attachment: ChatTypes.Attachment): ChatTypes.Attachment => ({
...attachment,
url: getFileUrl(attachment.name),
}));
const newMessage = {
id: new Guid().toString(),
...message,
attachments: attachmentsWithUrls,
};
dataSource.store().push([{
type: 'insert',
key: newMessage.id,
data: newMessage,
}]);
}, []);
const onAttachmentDownloadClick = useCallback((
{ attachment }: ChatTypes.AttachmentDownloadClickEvent,
): void => {
if (!attachment?.url) {
return;
}
const link = document.createElement('a');
link.setAttribute('href', attachment.url);
link.setAttribute('download', attachment.name);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, []);
const uploadFile = useCallback(() => {}, []);
return (
<>
<div className="chat-container">
<Chat
height={710}
dataSource={dataSource}
reloadOnChange={false}
user={currentUser}
onMessageEntered={onMessageEntered}
onAttachmentDownloadClick={onAttachmentDownloadClick}
>
<FileUploaderOptions
uploadFile={uploadFile}
onUploaded={onUploaded}
/>
</Chat>
</div>
</>
);
}
import React, { useCallback, useRef } from 'react';
import Chat, { FileUploaderOptions } from 'devextreme-react/chat';
import { Guid } from 'devextreme-react/common';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import { currentUser, messages as initialMessages } from './data.js';
const store = [...initialMessages];
const customStore = new CustomStore({
key: 'id',
load: () => Promise.resolve(store),
insert: (message) => {
store.push(message);
return Promise.resolve(message);
},
});
const dataSource = new DataSource({
store: customStore,
paginate: false,
});
export default function App() {
const uploadedFilesMapRef = useRef(new Map());
function getFileUrl(filename) {
return uploadedFilesMapRef.current.get(filename);
}
const onUploaded = useCallback((e) => {
const { file } = e;
const url = URL.createObjectURL(file);
uploadedFilesMapRef.current.set(file.name, url);
}, []);
const onMessageEntered = useCallback(({ message }) => {
const attachmentsWithUrls = message.attachments?.map((attachment) => ({
...attachment,
url: getFileUrl(attachment.name),
}));
const newMessage = {
id: new Guid().toString(),
...message,
attachments: attachmentsWithUrls,
};
dataSource.store().push([
{
type: 'insert',
key: newMessage.id,
data: newMessage,
},
]);
}, []);
const onAttachmentDownloadClick = useCallback(({ attachment }) => {
if (!attachment?.url) {
return;
}
const link = document.createElement('a');
link.setAttribute('href', attachment.url);
link.setAttribute('download', attachment.name);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, []);
const uploadFile = useCallback(() => {}, []);
return (
<>
<div className="chat-container">
<Chat
height={710}
dataSource={dataSource}
reloadOnChange={false}
user={currentUser}
onMessageEntered={onMessageEntered}
onAttachmentDownloadClick={onAttachmentDownloadClick}
>
<FileUploaderOptions
uploadFile={uploadFile}
onUploaded={onUploaded}
/>
</Chat>
</div>
</>
);
}
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: Date, offsetMinutes = 0) {
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: ChatTypes.Message[] = [
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: currentUser,
text: 'Hi! I\'m having trouble accessing my account.\nThe website says my password is incorrect. I\'m sending a few screenshots so you can see where I get the error.',
attachments: [
{
name: 'Pic1.png',
url: '../../../../images/Chat/FileAttachments/Pic1.png',
size: 1024 * 10,
},
{
name: 'Pic2.png',
url: '../../../../images/Chat/FileAttachments/Pic2.png',
size: 1024 * 10,
},
{
name: 'Pic3.png',
url: '../../../../images/Chat/FileAttachments/Pic3.png',
size: 1024 * 10,
},
],
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: supportAgent,
text: 'Hello! Thanks for including screenshots. To restore access, please follow instructions in the attached file.\nLet me know if you need anything else.',
attachments: [
{
name: 'Instructions.pdf',
url: '../../../../images/Chat/FileAttachments/Instructions.pdf',
size: 1024 * 10,
},
],
},
];
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/',
'anti-forgery:': '../../../../shared/anti-forgery/',
},
defaultExtension: 'js',
map: {
'anti-forgery': 'anti-forgery:fetch-override.js',
'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.9/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.29/dist/dx-diagram.js',
'devexpress-gantt': 'npm:devexpress-gantt@4.1.69/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.14.1/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,
},
};
window.process = {
env: {
NODE_ENV: 'production',
},
};
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, -7),
author: currentUser,
text: "Hi! I'm having trouble accessing my account.\nThe website says my password is incorrect. I'm sending a few screenshots so you can see where I get the error.",
attachments: [
{
name: 'Pic1.png',
url: '../../../../images/Chat/FileAttachments/Pic1.png',
size: 1024 * 10,
},
{
name: 'Pic2.png',
url: '../../../../images/Chat/FileAttachments/Pic2.png',
size: 1024 * 10,
},
{
name: 'Pic3.png',
url: '../../../../images/Chat/FileAttachments/Pic3.png',
size: 1024 * 10,
},
],
},
{
id: new Guid().toString(),
timestamp: getTimestamp(date, -7),
author: supportAgent,
text: 'Hello! Thanks for including screenshots. To restore access, please follow instructions in the attached file.\nLet me know if you need anything else.',
attachments: [
{
name: 'Instructions.pdf',
url: '../../../../images/Chat/FileAttachments/Instructions.pdf',
size: 1024 * 10,
},
],
},
];
<!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.2.8/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>
.demo-container {
min-width: 720px;
}
.chat-container {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
}
.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);
}
You can customize the file upload process with the following fileUploaderOptions properties:
- maxFileSize
Specifies maximum allowed file size. - minFileSize
Specifies minimum allowed file size. - multiple
When set tofalse, limits uploads to a single file. - allowedFileExtensions
Restricts accepted file types.
For the complete list of configuration options, refer to the following API section: fileUploaderOptions.
Attachment type includes name and size fields. To add custom fields (such as url in this demo), handle the onMessageEntered event and update the message object’s attachments array as needed. You can use this handler to save files to your server.
After a user sends a message, attachments appear in the corresponding message bubble. To allow users to download attachments, implement the onAttachmentDownloadClick event handler. You can define custom download logic within the handler.