@@ -15,6 +15,7 @@ import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChanne
1515import { sendMessageApi } from '../../mock-builders/api/sendMessage' ;
1616import { sendReactionApi } from '../../mock-builders/api/sendReaction' ;
1717import { useMockedApis } from '../../mock-builders/api/useMockedApis' ;
18+ import { generateFileReference } from '../../mock-builders/attachments' ;
1819import dispatchConnectionChangedEvent from '../../mock-builders/event/connectionChanged' ;
1920import { generateChannelResponse } from '../../mock-builders/generator/channel' ;
2021import { generateMember } from '../../mock-builders/generator/member' ;
@@ -397,6 +398,182 @@ export const OptimisticUpdates = () => {
397398 } ) ;
398399 } ) ;
399400
401+ describe ( 'edit message' , ( ) => {
402+ it ( 'should keep the optimistic edit in state and DB if the LLC queues the edit' , async ( ) => {
403+ const message = channel . state . messages [ 0 ] ;
404+ const editedText = 'edited while offline' ;
405+
406+ render (
407+ < Chat client = { chatClient } enableOfflineSupport >
408+ < Channel
409+ channel = { channel }
410+ doUpdateMessageRequest = { async ( _channelId , localMessage , options ) => {
411+ await chatClient . offlineDb . addPendingTask ( {
412+ channelId : channel . id ,
413+ channelType : channel . type ,
414+ messageId : message . id ,
415+ payload : [ localMessage , undefined , options ] ,
416+ type : 'update-message' ,
417+ } ) ;
418+ return {
419+ message : {
420+ ...localMessage ,
421+ message_text_updated_at : new Date ( ) ,
422+ updated_at : new Date ( ) ,
423+ } ,
424+ } ;
425+ } }
426+ >
427+ < CallbackEffectWithContext
428+ callback = { async ( { editMessage } ) => {
429+ await editMessage ( {
430+ localMessage : {
431+ ...message ,
432+ cid : channel . cid ,
433+ text : editedText ,
434+ } ,
435+ options : { } ,
436+ } ) ;
437+ } }
438+ context = { MessageInputContext }
439+ >
440+ < View testID = 'children' />
441+ </ CallbackEffectWithContext >
442+ </ Channel >
443+ </ Chat > ,
444+ ) ;
445+
446+ await waitFor ( ( ) => expect ( screen . getByTestId ( 'children' ) ) . toBeTruthy ( ) ) ;
447+
448+ await waitFor ( async ( ) => {
449+ const updatedMessage = channel . state . findMessage ( message . id ) ;
450+ const pendingTasksRows = await BetterSqlite . selectFromTable ( 'pendingTasks' ) ;
451+ const dbMessages = await BetterSqlite . selectFromTable ( 'messages' ) ;
452+ const dbMessage = dbMessages . find ( ( row ) => row . id === message . id ) ;
453+
454+ expect ( updatedMessage . text ) . toBe ( editedText ) ;
455+ expect ( updatedMessage . message_text_updated_at ) . toBeTruthy ( ) ;
456+ expect ( pendingTasksRows ) . toHaveLength ( 1 ) ;
457+ expect ( pendingTasksRows [ 0 ] . type ) . toBe ( 'update-message' ) ;
458+ expect ( dbMessage . text ) . toBe ( editedText ) ;
459+ expect ( dbMessage . messageTextUpdatedAt ) . toBeTruthy ( ) ;
460+ } ) ;
461+ } ) ;
462+
463+ it ( 'should rollback the optimistic edit if the request fails and no replayable task exists' , async ( ) => {
464+ const message = channel . state . messages [ 0 ] ;
465+ const originalText = message . text ;
466+
467+ render (
468+ < Chat client = { chatClient } enableOfflineSupport >
469+ < Channel
470+ channel = { channel }
471+ doUpdateMessageRequest = { ( ) => {
472+ throw new Error ( 'validation' ) ;
473+ } }
474+ >
475+ < CallbackEffectWithContext
476+ callback = { async ( { editMessage } ) => {
477+ try {
478+ await editMessage ( {
479+ localMessage : {
480+ ...message ,
481+ cid : channel . cid ,
482+ text : 'should rollback' ,
483+ } ,
484+ options : { } ,
485+ } ) ;
486+ } catch ( e ) {
487+ // do nothing
488+ }
489+ } }
490+ context = { MessageInputContext }
491+ >
492+ < View testID = 'children' />
493+ </ CallbackEffectWithContext >
494+ </ Channel >
495+ </ Chat > ,
496+ ) ;
497+
498+ await waitFor ( ( ) => expect ( screen . getByTestId ( 'children' ) ) . toBeTruthy ( ) ) ;
499+
500+ await waitFor ( async ( ) => {
501+ const updatedMessage = channel . state . findMessage ( message . id ) ;
502+ const pendingTasksRows = await BetterSqlite . selectFromTable ( 'pendingTasks' ) ;
503+ const dbMessages = await BetterSqlite . selectFromTable ( 'messages' ) ;
504+ const dbMessage = dbMessages . find ( ( row ) => row . id === message . id ) ;
505+
506+ expect ( updatedMessage . text ) . toBe ( originalText ) ;
507+ expect ( pendingTasksRows ) . toHaveLength ( 0 ) ;
508+ expect ( dbMessage . text ) . toBe ( originalText ) ;
509+ } ) ;
510+ } ) ;
511+
512+ it ( 'should keep the optimistic edit for attachment updates without auto-queueing' , async ( ) => {
513+ const message = channel . state . messages [ 0 ] ;
514+ const editedText = 'edited attachment message' ;
515+ const localUri = 'file://edited-attachment.png' ;
516+
517+ render (
518+ < Chat client = { chatClient } enableOfflineSupport >
519+ < Channel
520+ channel = { channel }
521+ doUpdateMessageRequest = { ( ) => {
522+ throw new Error ( 'offline' ) ;
523+ } }
524+ >
525+ < CallbackEffectWithContext
526+ callback = { async ( { editMessage } ) => {
527+ try {
528+ await editMessage ( {
529+ localMessage : {
530+ ...message ,
531+ attachments : [
532+ {
533+ asset_url : localUri ,
534+ originalFile : generateFileReference ( {
535+ name : 'edited-attachment.png' ,
536+ type : 'image/png' ,
537+ uri : localUri ,
538+ } ) ,
539+ type : 'file' ,
540+ } ,
541+ ] ,
542+ cid : channel . cid ,
543+ text : editedText ,
544+ } ,
545+ options : { } ,
546+ } ) ;
547+ } catch ( e ) {
548+ // do nothing
549+ }
550+ } }
551+ context = { MessageInputContext }
552+ >
553+ < View testID = 'children' />
554+ </ CallbackEffectWithContext >
555+ </ Channel >
556+ </ Chat > ,
557+ ) ;
558+
559+ await waitFor ( ( ) => expect ( screen . getByTestId ( 'children' ) ) . toBeTruthy ( ) ) ;
560+
561+ await waitFor ( async ( ) => {
562+ const updatedMessage = channel . state . findMessage ( message . id ) ;
563+ const pendingTasksRows = await BetterSqlite . selectFromTable ( 'pendingTasks' ) ;
564+ const dbMessages = await BetterSqlite . selectFromTable ( 'messages' ) ;
565+ const dbMessage = dbMessages . find ( ( row ) => row . id === message . id ) ;
566+ const storedAttachments = JSON . parse ( dbMessage . attachments ) ;
567+
568+ expect ( updatedMessage . text ) . toBe ( editedText ) ;
569+ expect ( updatedMessage . attachments [ 0 ] . asset_url ) . toBe ( localUri ) ;
570+ expect ( pendingTasksRows ) . toHaveLength ( 0 ) ;
571+ expect ( dbMessage . text ) . toBe ( editedText ) ;
572+ expect ( storedAttachments [ 0 ] . asset_url ) . toBe ( localUri ) ;
573+ } ) ;
574+ } ) ;
575+ } ) ;
576+
400577 describe ( 'pending task execution' , ( ) => {
401578 it ( 'pending task should be executed after connection is recovered' , async ( ) => {
402579 const message = channel . state . messages [ 0 ] ;
0 commit comments