11import React , { useState , useCallback , useEffect , useMemo } from 'react' ;
22import { Chat , ChatTypes } from 'devextreme-react/chat'
3- import { Button } from " devextreme-react" ;
3+ import { Button , Toast } from ' devextreme-react' ;
44import type { Meta , StoryObj } from '@storybook/react' ;
55import DataSource from 'devextreme/data/data_source' ;
66import CustomStore from 'devextreme/data/custom_store' ;
@@ -17,10 +17,12 @@ import {
1717 regenerationMessage ,
1818 assistant ,
1919} from './data' ;
20- import { Popup } from 'devextreme-react/popup' ;
20+ import { Popup , ToolbarItem } from 'devextreme-react/popup' ;
2121import HTMLReactParser from 'html-react-parser' ;
2222
2323import './styles.css' ;
24+ import { Guid } from 'devextreme-react/cjs/common' ;
25+ import { Message } from 'devextreme/artifacts/npm/devextreme/ui/chat' ;
2426
2527const meta : Meta < typeof Chat > = {
2628 title : 'Components/Chat' ,
@@ -99,7 +101,7 @@ export const Overview: Story = {
99101 focusStateEnabled,
100102 } ) => {
101103 const [ messages , setMessages ] = useState ( items ) ;
102-
104+
103105 const onMessageEntered = useCallback ( ( { message } ) => {
104106 const updatedMessages = [ ...messages , message ] ;
105107
@@ -344,6 +346,9 @@ export const PopupIntegration: Story = {
344346 visible = { true }
345347 showCloseButton = { false }
346348 title = "Chat title"
349+ wrapperAttr = { {
350+ class : 'chat-popup-integration'
351+ } }
347352 position = { {
348353 my : 'right bottom' ,
349354 at : 'right bottom' ,
@@ -588,3 +593,200 @@ export const AIBotIntegration: Story = {
588593 ) ;
589594 }
590595}
596+
597+ export const Editing : Story = {
598+ args : {
599+ user : firstAuthor ,
600+ activeStateEnabled : true ,
601+ hoverStateEnabled : true ,
602+ focusStateEnabled : true ,
603+ width : "600px" ,
604+ height : "600px" ,
605+ allowDeleting : true ,
606+ allowUpdating : true ,
607+ useCustomMessageRender : false ,
608+ cancelMessageEditingStart : false ,
609+ cancelMessageDeleting : false ,
610+ allowOnlyLatinTextOnEdit : false ,
611+ } ,
612+ argTypes : {
613+ user : {
614+ control : 'select' ,
615+ options : [ firstAuthor . name , secondAuthor . name ] ,
616+ mapping : {
617+ [ firstAuthor . name ] : firstAuthor ,
618+ [ secondAuthor . name ] : secondAuthor ,
619+ } ,
620+ defaultValue : firstAuthor . name ,
621+ } ,
622+ useCustomMessageRender : {
623+ control : 'boolean' ,
624+ name : 'Use Custom Message Renderer' ,
625+ } ,
626+ cancelMessageEditingStart : {
627+ control : 'boolean' ,
628+ name : 'Emulate Message Editing Cancellation' ,
629+ } ,
630+ cancelMessageDeleting : {
631+ control : 'boolean' ,
632+ name : 'Emulate Message Deleting Cancellation' ,
633+ } ,
634+ allowOnlyLatinTextOnEdit : {
635+ control : 'boolean' ,
636+ name : 'Allow only latin letters in message editing' ,
637+ }
638+ } ,
639+ render : ( {
640+ width,
641+ height,
642+ user,
643+ activeStateEnabled,
644+ hoverStateEnabled,
645+ focusStateEnabled,
646+ allowDeleting,
647+ allowUpdating,
648+ useCustomMessageRender,
649+ cancelMessageEditingStart,
650+ cancelMessageDeleting,
651+ allowOnlyLatinTextOnEdit,
652+ } ) => {
653+ const messages = useMemo ( ( ) => {
654+ const initial = initialMessages . map ( item => ( {
655+ ...item ,
656+ id : `dx-${ new Guid ( ) } ` ,
657+ } ) ) ;
658+ initial [ 4 ] . isDeleted = true ;
659+ initial [ 5 ] . isDeleted = true ;
660+ initial [ 6 ] . isDeleted = true ;
661+ return initial ;
662+ } , [ initialMessages ] ) ;
663+
664+ const [ toastConfig , setToastConfig ] = useState ( {
665+ visible : false ,
666+ message : '' ,
667+ } ) ;
668+
669+ const messagesRef = React . useRef ( [ ...messages ] ) ;
670+
671+ const dataSource = useMemo ( ( ) => new DataSource ( {
672+ store : new CustomStore ( {
673+ load : ( ) => new Promise ( resolve => setTimeout ( ( ) => resolve ( [ ...messagesRef . current ] ) , 500 ) ) ,
674+ insert : ( message ) => {
675+ message . id = `dx-${ new Guid ( ) } ` ;
676+ if ( message . author . id === user ?. id ) {
677+ message . author = {
678+ ...message . author ,
679+ ...user ,
680+ }
681+ }
682+ messagesRef . current . push ( message ) ;
683+ return new Promise < void > ( resolve => setTimeout ( resolve , 200 ) ) ;
684+ } ,
685+ key : 'id' ,
686+ } ) ,
687+ paginate : false ,
688+ } ) , [ user ] ) ;
689+
690+ const onUndoClick = useCallback ( ( message : Message ) => {
691+ const store = dataSource . store ( ) ;
692+ store . push ( [ { type : 'update' , key : message . id , data : { isDeleted : false } } ] ) ;
693+ } , [ dataSource ] ) ;
694+
695+ const messageRender = useCallback ( ( { message } ) => {
696+ if ( message . isDeleted === true ) {
697+ return (
698+ < div
699+ className = "dx-chat-messagebubble-content dx-chat-messagebubble-deleted"
700+ style = { {
701+ display : 'flex' ,
702+ gap : 4 ,
703+ alignItems : 'center' ,
704+ fontStyle : 'italic' ,
705+ } }
706+ >
707+ < div className = "dx-icon dx-icon-cursorprohibition" > </ div >
708+ < div > This message was deleted</ div >
709+ { user ?. id === message . author . id &&
710+ < a href = "#" onClick = { ( e ) => {
711+ e . preventDefault ( ) ;
712+ onUndoClick ( message ) ;
713+ } } > Undo</ a >
714+ }
715+ </ div >
716+ ) ;
717+ }
718+
719+ return < div > { message . text } </ div > ;
720+ } , [ user ] ) ;
721+
722+ const showToast = useCallback ( ( message : string ) => {
723+ setToastConfig ( {
724+ visible : true ,
725+ message,
726+ } ) ;
727+ } , [ ] ) ;
728+
729+ const onToastHiding = useCallback ( ( ) => {
730+ setToastConfig ( {
731+ visible : false ,
732+ message : '' ,
733+ } ) ;
734+ } , [ ] ) ;
735+
736+ const validateMessage = ( message : string ) => message . match ( / ^ [ a - z A - Z 0 - 9 . , ! ? ] + $ / ) ;
737+
738+ return (
739+ < div style = { { display : 'flex' , justifyContent : 'center' , alignItems : 'center' } } >
740+ < Chat
741+ editing = { { allowDeleting, allowUpdating } }
742+ width = { width }
743+ height = { height }
744+ dataSource = { dataSource }
745+ reloadOnChange = { false }
746+ user = { user }
747+ onMessageEntered = { ( e ) => {
748+ e . component . getDataSource ( ) . store ( ) . push ( [ { type : 'insert' , data : e . message } ] ) ;
749+ } }
750+ onMessageEditingStart = { async ( e ) => {
751+ if ( cancelMessageEditingStart ) {
752+ showToast ( 'Message editing not allowed' ) ;
753+ e . cancel = true ;
754+ }
755+ } }
756+ onMessageEditCanceled = { ( ) => {
757+ showToast ( 'Message editing is canceled' ) ;
758+ } }
759+ onMessageDeleting = { ( e ) => {
760+ if ( cancelMessageDeleting ) {
761+ showToast ( 'Message deleting was canceled' ) ;
762+ e . cancel = true ;
763+ }
764+ } }
765+ onMessageDeleted = { ( e ) => {
766+ e . component . getDataSource ( ) . store ( ) . push ( [ { type : 'update' , key : e . message . id , data : { isDeleted : true } } ] ) ;
767+ } }
768+ onMessageUpdating = { async ( e ) => {
769+ if ( allowOnlyLatinTextOnEdit ) {
770+ if ( ! validateMessage ( e . text ?? '' ) ) {
771+ showToast ( 'Only latin allowed in message' ) ;
772+ e . cancel = true ;
773+ }
774+ }
775+ } }
776+ onMessageUpdated = { ( e ) => {
777+ e . component . getDataSource ( ) . store ( ) . push ( [ { type : 'update' , key : e . message . id , data : { text : e . text , isEdited : true } } ] ) ;
778+ } }
779+ activeStateEnabled = { activeStateEnabled }
780+ focusStateEnabled = { focusStateEnabled }
781+ hoverStateEnabled = { hoverStateEnabled }
782+ { ...( useCustomMessageRender && { messageRender } ) }
783+ />
784+ < Toast
785+ { ...toastConfig }
786+ onHiding = { onToastHiding }
787+ displayTime = { 600 }
788+ />
789+ </ div >
790+ ) ;
791+ }
792+ } ;
0 commit comments