Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .maestro/tests/assorted/private-message.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
appId: chat.rocket.reactnative
name: Private Message
onFlowStart:
- runFlow: '../../helpers/setup.yaml'
onFlowComplete:
- evalScript: ${output.utils.deleteCreatedUsers()}
tags:
- test-13

---
- evalScript: ${output.user = output.utils.createUser()}
- evalScript: ${output.channel = output.utils.createRandomRoom(output.user.username, output.user.password)}

- runFlow:
file: '../../helpers/login-with-deeplink.yaml'
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}
- tapOn:
id: rooms-list-view-item-${output.channel.name}

# send a private slash command and dismiss it manually
- tapOn:
id: message-composer-input
- inputText: '/status'
- extendedWaitUntil:
visible:
id: autocomplete-item-status
timeout: 15000
- tapOn:
id: autocomplete-item-status
- extendedWaitUntil:
notVisible:
id: autocomplete-item-status
timeout: 15000
- tapOn:
id: message-composer-send
- extendedWaitUntil:
visible:
text: 'Status message changed successfully.'
timeout: 60000
- extendedWaitUntil:
visible:
text: 'Dismiss message'
timeout: 15000
- tapOn:
id: dismiss-private-message
- extendedWaitUntil:
notVisible:
text: 'Status message changed successfully.'
timeout: 15000

# send a private slash command, go back to room list and return to same channel
- tapOn:
id: message-composer-input
- inputText: '/status'
- extendedWaitUntil:
visible:
id: autocomplete-item-status
timeout: 15000
- tapOn:
id: autocomplete-item-status
- extendedWaitUntil:
notVisible:
id: autocomplete-item-status
timeout: 15000
- tapOn:
id: message-composer-send
- extendedWaitUntil:
visible:
text: 'Status message changed successfully.'
timeout: 60000
- extendedWaitUntil:
visible:
text: 'Dismiss message'
timeout: 15000
- tapOn:
id: header-back
- tapOn:
id: rooms-list-view-item-${output.channel.name}
- extendedWaitUntil:
notVisible:
text: 'Status message changed successfully.'
timeout: 15000
11 changes: 10 additions & 1 deletion app/containers/message/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import { View, type ViewStyle } from 'react-native';
import { View, Text, type ViewStyle, Pressable } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { A11y } from 'react-native-a11y-order';

Expand All @@ -26,6 +26,7 @@ import { getInfoMessage } from './utils';
import MessageTime from './Time';
import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout';
import Quote from './Components/Attachments/Quote';
import { deletePrivateMessages } from '../../lib/methods/deletePrivateMessages';
import translationLanguages from '../../lib/constants/translationLanguages';

const MessageInner = React.memo((props: IMessageInner) => {
Expand Down Expand Up @@ -193,6 +194,14 @@ const Message = React.memo((props: IMessageTouchable & IMessage) => {
<MessageAvatar {...props} />
<View style={styles.messageContent}>
<MessageInner {...props} />
{props.private && (
<View style={styles.privateIndicator}>
<Text style={styles.privateIndicatorText}>{i18n.t('Only_you_can_see_this_message')} • </Text>
<Pressable testID='dismiss-private-message' onPress={() => deletePrivateMessages(props.id)}>
<Text style={styles.privateMessageDismiss}>{i18n.t('Dismiss_message')}</Text>
</Pressable>
</View>
)}
Comment on lines +197 to +204
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add user feedback and error handling for dismiss action.

The dismiss action calls deletePrivateMessages without awaiting or providing feedback. Users won't know if the dismissal succeeded or failed.

Consider these improvements:

  1. Add a loading state while deleting
  2. Show success/error feedback (toast/snackbar)
  3. Disable the button during deletion to prevent multiple taps
  4. Handle potential errors gracefully

Example implementation:

+const [isDismissing, setIsDismissing] = React.useState(false);
+
+const handleDismiss = async () => {
+  if (isDismissing) return;
+  setIsDismissing(true);
+  try {
+    await deletePrivateMessages(props.id);
+    // Show success feedback (e.g., toast)
+  } catch (error) {
+    // Show error feedback
+  } finally {
+    setIsDismissing(false);
+  }
+};

 {props.private && (
   <View style={styles.privateIndicator}>
     <Text style={styles.privateIndicatorText}>{i18n.t('Only_you_can_see_this_message')} • </Text>
-    <Pressable testID='dismiss-private-message' onPress={() => deletePrivateMessages(props.id)}>
-      <Text style={styles.privateMessageDismiss}>{i18n.t('Dismiss_message')}</Text>
+    <Pressable testID='dismiss-private-message' onPress={handleDismiss} disabled={isDismissing}>
+      <Text style={styles.privateMessageDismiss}>
+        {isDismissing ? i18n.t('Dismissing...') : i18n.t('Dismiss_message')}
+      </Text>
     </Pressable>
   </View>
 )}

Committable suggestion skipped: line range outside the PR's diff.

</View>
{!props.isHeader ? (
<RightIcons
Expand Down
4 changes: 3 additions & 1 deletion app/containers/message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
replies,
md,
comment,
pinned
pinned,
private: isPrivate
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't use private as variable name in class...

} = item;

let message = msg;
Expand Down Expand Up @@ -505,6 +506,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
isBeingEdited={isBeingEdited}
isPreview={isPreview}
pinned={pinned}
private={isPrivate}
autoTranslateLanguage={autoTranslateLanguage}
/>
<MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} />
Expand Down
1 change: 1 addition & 0 deletions app/containers/message/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface IMessageInner
blocks: [];
urls?: IUrl[];
isPreview?: boolean;
private?: boolean;
}

export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar {
Expand Down
12 changes: 12 additions & 0 deletions app/containers/message/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,17 @@ export default StyleSheet.create({
right: 8,
flexDirection: 'row',
gap: 8
},
privateIndicator: {
flexDirection: 'row',
marginTop: 5
},
privateIndicatorText: {
fontSize: 12,
...sharedStyles.textRegular
},
privateMessageDismiss: {
fontSize: 12,
...sharedStyles.textRegular
}
});
1 change: 1 addition & 0 deletions app/definitions/IMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export interface IMessage extends IMessageFromServer {
subscription?: { id: string };
user?: string;
editedAt?: string | Date;
private?: boolean;
e2eMentions?: any;
}

Expand Down
2 changes: 2 additions & 0 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
"Discussion_name": "Discussion name",
"Discussion_name_required": "Discussion name required",
"Discussions": "Discussions",
"Dismiss_message": "Dismiss message",
"Display": "Display",
"Displays_action_text": "Displays action text",
"Do_it_later": "Do it later",
Expand Down Expand Up @@ -612,6 +613,7 @@
"One_result_found": "One result found.",
"Online": "Online",
"Only_authorized_users_can_write_new_messages": "Only authorized users can write new messages",
"Only_you_can_see_this_message": "Only you can see this message",
"Oops": "Oops!",
"Open_Livechats": "Omnichannel chats in progress",
"Open_servers_history": "Open servers history",
Expand Down
5 changes: 4 additions & 1 deletion app/lib/database/model/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export default class Message extends Model {

@field('comment') comment;

@field('private') private;

asPlain() {
return {
id: this.id,
Expand Down Expand Up @@ -128,7 +130,8 @@ export default class Message extends Model {
tshow: this.tshow,
md: this.md,
content: this.content,
comment: this.comment
comment: this.comment,
private: this.private
};
}
}
9 changes: 9 additions & 0 deletions app/lib/database/model/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ export default schemaMigrations({
columns: [{ name: 'federated', type: 'boolean', isOptional: true }]
})
]
},
{
toVersion: 28,
steps: [
addColumns({
table: 'messages',
columns: [{ name: 'private', type: 'boolean', isOptional: true }]
})
]
}
]
});
5 changes: 3 additions & 2 deletions app/lib/database/schema/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';

export default appSchema({
version: 27,
version: 28,
tables: [
tableSchema({
name: 'subscriptions',
Expand Down Expand Up @@ -129,7 +129,8 @@ export default appSchema({
{ name: 'tshow', type: 'boolean', isOptional: true },
{ name: 'md', type: 'string', isOptional: true },
{ name: 'content', type: 'string', isOptional: true },
{ name: 'comment', type: 'string', isOptional: true }
{ name: 'comment', type: 'string', isOptional: true },
{ name: 'private', type: 'boolean', isOptional: true }
]
}),
tableSchema({
Expand Down
26 changes: 26 additions & 0 deletions app/lib/methods/deletePrivateMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Q } from '@nozbe/watermelondb';

import database from '../database';
import log from './helpers/log';

export async function deletePrivateMessages(id?: string): Promise<void> {
try {
const db = database.active;

const messages = id
? await db.get('messages').query(Q.where('id', id)).fetch()
: await db.get('messages').query(Q.where('private', true)).fetch();
const messagesToBeDeleted = messages.map(message => message.prepareDestroyPermanently());

await db.write(async () => {
try {
await db.batch(...messagesToBeDeleted);
} catch (e) {
console.log('e', e);
// Do nothing
}
});
Comment on lines +15 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve error handling - don't silently swallow batch errors.

The inner try-catch at lines 16-21 swallows errors with only a console.log, making debugging difficult and preventing callers from knowing if deletion failed.

Consider these improvements:

 await db.write(async () => {
-  try {
-    await db.batch(...messagesToBeDeleted);
-  } catch (e) {
-    console.log('e', e);
-    // Do nothing
-  }
+  await db.batch(...messagesToBeDeleted);
 });

If you need to handle specific batch errors, re-throw them so the outer catch can log properly:

 await db.write(async () => {
   try {
     await db.batch(...messagesToBeDeleted);
   } catch (e) {
-    console.log('e', e);
-    // Do nothing
+    log(e);
+    throw e; // Re-throw so caller knows deletion failed
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await db.write(async () => {
try {
await db.batch(...messagesToBeDeleted);
} catch (e) {
console.log('e', e);
// Do nothing
}
});
await db.write(async () => {
await db.batch(...messagesToBeDeleted);
});
Suggested change
await db.write(async () => {
try {
await db.batch(...messagesToBeDeleted);
} catch (e) {
console.log('e', e);
// Do nothing
}
});
await db.write(async () => {
try {
await db.batch(...messagesToBeDeleted);
} catch (e) {
log(e);
throw e; // Re-throw so caller knows deletion failed
}
});
🤖 Prompt for AI Agents
In app/lib/methods/deletePrivateMessages.ts around lines 15 to 22 the inner
try-catch is swallowing batch errors (only doing console.log and continuing), so
replace that silent catch with proper error propagation: either remove the inner
try-catch entirely so a thrown error bubbles to the outer catch, or if you must
log here, log using the application's logger with a descriptive message and then
re-throw the error so the outer catch can handle/report it; ensure the logged
message includes the error object and context (which messages failed) instead of
console.log.

} catch (e) {
log(e);
}
}
8 changes: 8 additions & 0 deletions app/views/RoomView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import UserPreferences from '../../lib/methods/userPreferences';
import { type IRoomViewProps, type IRoomViewState } from './definitions';
import { roomAttrsUpdate, stateAttrsUpdate } from './constants';
import { EncryptedRoom, MissingRoomE2EEKey } from './components';
import { deletePrivateMessages } from '../../lib/methods/deletePrivateMessages';

class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
private rid?: string;
Expand Down Expand Up @@ -370,6 +371,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
}
EventEmitter.removeListener('connected', this.handleConnected);
EventEmitter.removeListener('ROOM_REMOVED', this.handleRoomRemoved);

deletePrivateMessages();
if (!this.tmid) {
await AudioManager.unloadRoomAudios(this.rid);
}
Expand Down Expand Up @@ -860,6 +863,11 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if (action && action !== 'quote') {
return;
}

if (message.private) {
return;
}

// if it's a thread message on main room, we disable the long press
if (message.tmid && !this.tmid) {
return;
Expand Down
Loading