Skip to content

Commit 80bb523

Browse files
feat: optimistic reaction update
1 parent 566e100 commit 80bb523

File tree

2 files changed

+80
-12
lines changed

2 files changed

+80
-12
lines changed

app/containers/message/index.tsx

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import React from 'react';
2-
import { Keyboard } from 'react-native';
2+
import { Alert, Keyboard } from 'react-native';
33

44
import Message from './Message';
55
import MessageContext from './Context';
66
import { debounce } from '../../lib/methods/helpers';
77
import { getMessageTranslation } from './utils';
88
import { type TSupportedThemes, withTheme } from '../../theme';
99
import 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';
1111
import { type IRoomInfoParam } from '../../views/SearchMessagesView';
1212
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys';
1313
import { messagesStatus } from '../../lib/constants/messagesStatus';
1414
import MessageSeparator from '../MessageSeparator';
15+
import i18n from '../../i18n';
1516

1617
interface 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

6869
interface IMessageContainerState {
6970
isManualUnignored: boolean;
71+
// for optimistic reaction updates
72+
proxyReactions?: IReaction[];
7073
}
7174

7275
class 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;

app/views/RoomView/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,9 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
906906
Review.pushPositiveEvent();
907907
} catch (e) {
908908
log(e);
909+
return false;
909910
}
911+
return true;
910912
};
911913

912914
onReactionLongPress = (message: TAnyMessageModel) => {

0 commit comments

Comments
 (0)