diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx
index 466a70ce510..6496acbccfd 100644
--- a/app/containers/MessageActions/index.tsx
+++ b/app/containers/MessageActions/index.tsx
@@ -6,6 +6,9 @@ import { connect } from 'react-redux';
import dayjs from '../../lib/dayjs';
import database from '../../lib/database';
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
+import { getMessageById } from '../../lib/database/services/Message';
+import protectedFunction from '../../lib/methods/helpers/protectedFunction';
+import { registerOptimisticUpdate } from '../../lib/methods/helpers/optimisticUpdates';
import I18n from '../../i18n';
import log, { logEvent } from '../../lib/methods/helpers/log';
import Navigation from '../../lib/navigation/appNavigation';
@@ -306,12 +309,47 @@ const MessageActions = React.memo(
}
};
- const handleStar = async (messageId: string, starred: boolean) => {
- logEvent(starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
+ const handleStar = async (message: TAnyMessageModel) => {
+ const currentStarred = Boolean(message.starred);
+ const willStar = !currentStarred;
+ logEvent(events.ROOM_MSG_ACTION_STAR);
+
try {
- await toggleStarMessage(messageId, starred);
- EventEmitter.emit(LISTENER, { message: starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
+ const db = database.active;
+ const messageRecord = await getMessageById(message.id);
+ if (messageRecord) {
+ await db.write(async () => {
+ await messageRecord.update(
+ protectedFunction((m: any) => {
+ m.starred = willStar;
+ })
+ );
+ });
+ registerOptimisticUpdate(message.id, { starred: willStar });
+ }
+ } catch (optimisticError) {
+ // Do nothing
+ }
+
+ try {
+ await toggleStarMessage(message.id, currentStarred);
+ EventEmitter.emit(LISTENER, { message: willStar ? I18n.t('Message_starred') : I18n.t('Message_unstarred') });
} catch (e) {
+ try {
+ const db = database.active;
+ const messageRecord = await getMessageById(message.id);
+ if (messageRecord) {
+ await db.write(async () => {
+ await messageRecord.update(
+ protectedFunction((m: any) => {
+ m.starred = currentStarred;
+ })
+ );
+ });
+ }
+ } catch (revertError) {
+ // Do nothing
+ }
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
@@ -506,7 +544,7 @@ const MessageActions = React.memo(
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
- onPress: () => handleStar(message.id, message.starred || false)
+ onPress: () => handleStar(message)
});
}
diff --git a/app/containers/message/Components/RightIcons/Starred.tsx b/app/containers/message/Components/RightIcons/Starred.tsx
new file mode 100644
index 00000000000..62787be3f4b
--- /dev/null
+++ b/app/containers/message/Components/RightIcons/Starred.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { CustomIcon } from '../../../CustomIcon';
+import styles from '../../styles';
+
+const Starred = ({ starred, testID }: { starred?: boolean; testID?: string }): React.ReactElement | null => {
+ 'use memo';
+
+ if (starred) return ;
+ return null;
+};
+
+export default Starred;
diff --git a/app/containers/message/Components/RightIcons/index.tsx b/app/containers/message/Components/RightIcons/index.tsx
index 6421f0c1ed8..a544a3b6c0c 100644
--- a/app/containers/message/Components/RightIcons/index.tsx
+++ b/app/containers/message/Components/RightIcons/index.tsx
@@ -7,6 +7,7 @@ import Encrypted from './Encrypted';
import MessageError from './MessageError';
import Pinned from './Pinned';
import ReadReceipt from './ReadReceipt';
+import Starred from './Starred';
import Translated from './Translated';
const styles = StyleSheet.create({
@@ -24,6 +25,7 @@ interface IRightIcons {
hasError: boolean;
isTranslated: boolean;
pinned?: boolean;
+ starred?: boolean;
}
const RightIcons = ({
@@ -34,13 +36,15 @@ const RightIcons = ({
isReadReceiptEnabled,
unread,
isTranslated,
- pinned
+ pinned,
+ starred
}: IRightIcons): React.ReactElement => {
'use memo';
return (
+
diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx
index ce32c546e6a..3d79828a22d 100644
--- a/app/containers/message/Message.tsx
+++ b/app/containers/message/Message.tsx
@@ -204,6 +204,7 @@ const Message = React.memo((props: IMessageTouchable & IMessage) => {
isReadReceiptEnabled={props.isReadReceiptEnabled}
unread={props.unread}
pinned={props.pinned}
+ starred={props.starred}
isTranslated={props.isTranslated}
/>
) : null}
diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx
index f24a65d849f..70ba0284888 100644
--- a/app/containers/message/User.tsx
+++ b/app/containers/message/User.tsx
@@ -62,6 +62,7 @@ interface IMessageUser {
isReadReceiptEnabled?: boolean;
unread?: boolean;
pinned?: boolean;
+ starred?: boolean;
isTranslated: boolean;
}
@@ -129,6 +130,7 @@ const User = React.memo(
isReadReceiptEnabled={props.isReadReceiptEnabled}
unread={props.unread}
pinned={props.pinned}
+ starred={props.starred}
isTranslated={isTranslated}
/>
diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx
index 31522bd7f5c..d6bcb541bbe 100644
--- a/app/containers/message/index.tsx
+++ b/app/containers/message/index.tsx
@@ -410,7 +410,8 @@ class MessageContainer extends React.Component
diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts
index 8523f7836b3..a64afd73945 100644
--- a/app/containers/message/interfaces.ts
+++ b/app/containers/message/interfaces.ts
@@ -71,6 +71,7 @@ export interface IMessageContent {
isHeader: boolean;
isTranslated: boolean;
pinned?: boolean;
+ starred?: boolean;
}
export interface IMessageEmoji {
diff --git a/app/lib/methods/helpers/optimisticUpdates.ts b/app/lib/methods/helpers/optimisticUpdates.ts
new file mode 100644
index 00000000000..ce30502c241
--- /dev/null
+++ b/app/lib/methods/helpers/optimisticUpdates.ts
@@ -0,0 +1,43 @@
+interface IOptimisticUpdate {
+ pinned?: boolean;
+ starred?: boolean;
+ timestamp: number;
+}
+
+const optimisticUpdates: Map = new Map();
+const CLEANUP_THRESHOLD = 10000;
+
+const cleanupStaleUpdates = (maxAge: number = CLEANUP_THRESHOLD) => {
+ const now = Date.now();
+ for (const [messageId, update] of optimisticUpdates.entries()) {
+ const age = now - update.timestamp;
+ if (age >= maxAge) {
+ optimisticUpdates.delete(messageId);
+ }
+ }
+};
+
+export const registerOptimisticUpdate = (messageId: string, update: { pinned?: boolean; starred?: boolean }) => {
+ cleanupStaleUpdates();
+ optimisticUpdates.set(messageId, {
+ ...update,
+ timestamp: Date.now()
+ });
+};
+
+export const getOptimisticUpdate = (messageId: string): IOptimisticUpdate | undefined => {
+ cleanupStaleUpdates();
+ return optimisticUpdates.get(messageId);
+};
+
+export const clearOptimisticUpdate = (messageId: string) => {
+ optimisticUpdates.delete(messageId);
+};
+
+export const isRecentOptimisticUpdate = (messageId: string, maxAge: number = 2000): boolean => {
+ cleanupStaleUpdates();
+ const update = optimisticUpdates.get(messageId);
+ if (!update) return false;
+ const age = Date.now() - update.timestamp;
+ return age < maxAge;
+};
diff --git a/app/lib/methods/subscriptions/room.ts b/app/lib/methods/subscriptions/room.ts
index a0ce1310b24..6710069a12f 100644
--- a/app/lib/methods/subscriptions/room.ts
+++ b/app/lib/methods/subscriptions/room.ts
@@ -29,6 +29,7 @@ import { readMessages } from '../readMessages';
import { loadMissedMessages } from '../loadMissedMessages';
import { updateLastOpen } from '../updateLastOpen';
import markMessagesRead from '../helpers/markMessagesRead';
+import { getOptimisticUpdate, isRecentOptimisticUpdate } from '../helpers/optimisticUpdates';
export default class RoomSubscription {
private rid: string;
@@ -281,7 +282,27 @@ export default class RoomSubscription {
batch.push(
messageRecord.prepareUpdate(
protectedFunction((m: TMessageModel) => {
- Object.assign(m, message);
+ const optimisticUpdate = getOptimisticUpdate(message._id);
+ const isRecentOptimistic = isRecentOptimisticUpdate(message._id, 2000);
+
+ const { pinned: _pinned, starred: _starred, ...restMessage } = message;
+ Object.assign(m, restMessage);
+
+ if (message.pinned !== undefined) {
+ if (isRecentOptimistic && optimisticUpdate?.pinned !== undefined) {
+ m.pinned = optimisticUpdate.pinned;
+ } else {
+ m.pinned = message.pinned;
+ }
+ }
+
+ if (message.starred !== undefined) {
+ if (isRecentOptimistic && optimisticUpdate?.starred !== undefined) {
+ m.starred = optimisticUpdate.starred;
+ } else {
+ m.starred = message.starred;
+ }
+ }
})
)
);