11import React from 'react' ;
2- import { Keyboard } from 'react-native' ;
2+ import { Alert , Keyboard } from 'react-native' ;
33
44import Message from './Message' ;
55import MessageContext from './Context' ;
66import { debounce } from '../../lib/methods/helpers' ;
77import { getMessageTranslation } from './utils' ;
88import { type TSupportedThemes , withTheme } from '../../theme' ;
99import openLink from '../../lib/methods/helpers/openLink' ;
10- import { type IAttachment , type TAnyMessageModel , type TGetCustomEmoji } from '../../definitions' ;
10+ import { type IReaction , type IAttachment , type TAnyMessageModel , type TGetCustomEmoji } from '../../definitions' ;
1111import { type IRoomInfoParam } from '../../views/SearchMessagesView' ;
1212import { E2E_MESSAGE_TYPE , E2E_STATUS } from '../../lib/constants/keys' ;
1313import { messagesStatus } from '../../lib/constants/messagesStatus' ;
1414import MessageSeparator from '../MessageSeparator' ;
15+ import i18n from '../../i18n' ;
1516
1617interface IMessageContainerProps {
1718 item : TAnyMessageModel ;
@@ -39,7 +40,7 @@ interface IMessageContainerProps {
3940 highlighted ?: boolean ;
4041 getCustomEmoji : TGetCustomEmoji ;
4142 onLongPress ?: ( item : TAnyMessageModel ) => void ;
42- onReactionPress ?: ( emoji : string , id : string ) => void ;
43+ onReactionPress ?: ( emoji : string , id : string ) => Promise < boolean > | void ;
4344 onEncryptedPress ?: ( ) => void ;
4445 onDiscussionPress ?: ( item : TAnyMessageModel ) => void ;
4546 onThreadPress ?: ( item : TAnyMessageModel ) => void ;
@@ -67,6 +68,8 @@ interface IMessageContainerProps {
6768
6869interface IMessageContainerState {
6970 isManualUnignored : boolean ;
71+ // for optimistic reaction updates
72+ proxyReactions ?: IReaction [ ] ;
7073}
7174
7275class MessageContainer extends React . Component < IMessageContainerProps , IMessageContainerState > {
@@ -80,7 +83,10 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
8083 theme : 'light' as TSupportedThemes
8184 } ;
8285
83- state = { isManualUnignored : false } ;
86+ /**
87+ * set undefined when we are using value from server
88+ */
89+ state = { isManualUnignored : false , proxyReactions : undefined } ;
8490
8591 private subscription ?: Function ;
8692
@@ -92,13 +98,14 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
9298 // experimentalSubscribe(subscriber: (isDeleted: boolean) => void, debugInfo?: any): Unsubscribe
9399 // @ts -ignore
94100 this . subscription = item . experimentalSubscribe ( ( ) => {
95- this . forceUpdate ( ) ;
101+ this . setState ( { proxyReactions : undefined } ) ;
96102 } ) ;
97103 }
98104 }
99105
100106 shouldComponentUpdate ( nextProps : IMessageContainerProps , nextState : IMessageContainerState ) {
101- const { isManualUnignored } = this . state ;
107+ const { isManualUnignored, proxyReactions } = this . state ;
108+
102109 const {
103110 threadBadgeColor,
104111 isIgnored,
@@ -108,9 +115,18 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
108115 autoTranslateLanguage,
109116 isBeingEdited,
110117 showUnreadSeparator,
111- dateSeparator
118+ dateSeparator,
119+ item
112120 } = this . props ;
113121
122+ // optimistic UI updates
123+ if ( nextState . proxyReactions !== proxyReactions ) {
124+ return true ;
125+ }
126+
127+ if ( nextProps . item . reactions !== item . reactions ) {
128+ return true ;
129+ }
114130 if ( nextProps . showUnreadSeparator !== showUnreadSeparator ) {
115131 return true ;
116132 }
@@ -205,10 +221,57 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
205221 }
206222 } ;
207223
208- onReactionPress = ( emoji : string ) => {
209- const { onReactionPress, item } = this . props ;
210- if ( onReactionPress ) {
211- onReactionPress ( emoji , item . id ) ;
224+ onReactionPress = async ( emoji : string ) => {
225+ const { onReactionPress, item, user } = this . props ;
226+ const { username } = user ;
227+
228+ this . setState ( prev => {
229+ const current = prev . proxyReactions ?? item . reactions ?? [ ] ;
230+
231+ const updated = [ ...current ] ;
232+ const index = updated . findIndex ( r => r . emoji === emoji ) ;
233+
234+ if ( index > - 1 ) {
235+ const alreadyReacted = updated [ index ] . usernames . includes ( username ) ;
236+
237+ if ( alreadyReacted ) {
238+ // remove
239+ const newUsers = updated [ index ] . usernames . filter ( u => u !== username ) ;
240+
241+ if ( newUsers . length === 0 ) {
242+ updated . splice ( index , 1 ) ;
243+ } else {
244+ updated [ index ] = {
245+ ...updated [ index ] ,
246+ usernames : newUsers
247+ } ;
248+ }
249+ } else {
250+ // add
251+ updated [ index ] = {
252+ ...updated [ index ] ,
253+ usernames : [ ...updated [ index ] . usernames , username ]
254+ } ;
255+ }
256+ } else {
257+ updated . push ( {
258+ _id : `${ emoji } -${ Date . now ( ) } ` ,
259+ emoji,
260+ usernames : [ username ] ,
261+ names : [ username ]
262+ } ) ;
263+ }
264+
265+ return { proxyReactions : updated } ;
266+ } ) ;
267+
268+ // still call server
269+
270+ const success = await onReactionPress ?.( emoji , item . id ) ;
271+ if ( ! success ) {
272+ Alert . alert ( i18n . t ( 'Error' ) , i18n . t ( 'Reaction_Failed' ) ) ;
273+ // rollback on failure
274+ this . setState ( { proxyReactions : undefined } ) ;
212275 }
213276 } ;
214277
@@ -387,7 +450,6 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
387450 ts,
388451 attachments,
389452 urls,
390- reactions,
391453 t,
392454 avatar,
393455 emoji,
@@ -413,6 +475,10 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
413475 pinned
414476 } = item ;
415477
478+ // extract reactions later for optimistic updates
479+ const serverReactions = item . reactions ;
480+ const reactions = this . state . proxyReactions ?? serverReactions ;
481+
416482 let message = msg ;
417483 let isTranslated = false ;
418484 const otherUserMessage = u ?. username !== user ?. username ;
0 commit comments