diff --git a/ts/data/data.ts b/ts/data/data.ts
index 9a33e41f6..8374850c4 100644
--- a/ts/data/data.ts
+++ b/ts/data/data.ts
@@ -493,9 +493,17 @@ async function getUnreadByConversation(conversationId: string): Promise<MessageC
 }
 
 async function markAllAsReadByConversationNoExpiration(
-  conversationId: string
+  conversationId: string,
+  returnMessagesUpdated: boolean // for performance reason we do not return them because usually they are not needed
 ): Promise<Array<number>> {
-  const messagesIds = await channels.markAllAsReadByConversationNoExpiration(conversationId);
+  // tslint:disable-next-line: no-console
+  console.time('markAllAsReadByConversationNoExpiration');
+  const messagesIds = await channels.markAllAsReadByConversationNoExpiration(
+    conversationId,
+    returnMessagesUpdated
+  );
+  // tslint:disable-next-line: no-console
+  console.timeEnd('markAllAsReadByConversationNoExpiration');
   return messagesIds;
 }
 
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts
index bb7059e56..6c13eafa9 100644
--- a/ts/interactions/conversationInteractions.ts
+++ b/ts/interactions/conversationInteractions.ts
@@ -254,11 +254,7 @@ export function showLeaveGroupByConvoId(conversationId: string) {
 export function showInviteContactByConvoId(conversationId: string) {
   window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));
 }
-export async function onMarkAllReadByConvoId(conversationId: string) {
-  const conversation = getConversationController().get(conversationId);
 
-  await conversation.markReadBouncy(Date.now());
-}
 
 export function showAddModeratorsByConvoId(conversationId: string) {
   window.inboxStore?.dispatch(updateAddModeratorsModal({ conversationId }));
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index fe3942186..266629ee6 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -32,6 +32,7 @@ import {
   actions as conversationActions,
   conversationChanged,
   conversationsChanged,
+  markConversationFullyRead,
   MessageModelPropsWithoutConvoProps,
   ReduxConversationType,
 } from '../state/ducks/conversations';
@@ -1231,27 +1232,38 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
    * Send read receipt if needed.
    */
   public async markAllAsRead() {
-    if (this.isOpenGroupV2()) {
+    /**
+     *  when marking all as read, there is a bunch of things we need to do.
+     *   - we need to update all the messages in the DB not read yet for that conversation
+     *   - we need to send the read receipts if there is one needed for those messages
+     *   - we need to trigger a change on the redux store, so those messages are read AND mark the whole convo as read.
+     *   - we need to remove any notifications related to this conversation ID.
+     *
+     *
+     * (if there is an expireTimer, we do it the slow way, handling each message separately)
+     */
+    const expireTimerSet = !!this.get('expireTimer');
+    if (this.isOpenGroupV2() || !expireTimerSet) {
       // for opengroups, we batch everything as there is no expiration timer to take care (and potentially a lot of messages)
 
-      await Data.markAllAsReadByConversationNoExpiration(this.id);
+      const isOpenGroup = this.isOpenGroupV2();
+      // if this is an opengroup there is no need to send read receipt, and so no need to fetch messages updated.
+      const allReadMessages = await Data.markAllAsReadByConversationNoExpiration(
+        this.id,
+        !isOpenGroup
+      );
       this.set({ mentionedUs: false, unreadCount: 0 });
 
       await this.commit();
-      return;
-    }
-
-    // if the conversation has no expiration timer, we can also batch everything, but we also need to send read receipts potentially
-    // so we grab them from the db
-    if (!this.get('expireTimer')) {
-      const allReadMessages = await Data.markAllAsReadByConversationNoExpiration(this.id);
-      this.set({ mentionedUs: false, unreadCount: 0 });
-      await this.commit();
-      if (allReadMessages.length) {
+      if (!this.isOpenGroupV2() && allReadMessages.length) {
         await this.sendReadReceiptsIfNeeded(uniq(allReadMessages));
       }
+      Notifications.clearByConversationID(this.id);
+      window.inboxStore?.dispatch(markConversationFullyRead(this.id));
+
       return;
     }
+    // otherwise, do it the slow way
     await this.markReadBouncy(Date.now());
   }
 
diff --git a/ts/node/database_utility.ts b/ts/node/database_utility.ts
index 7a77eca44..17039750e 100644
--- a/ts/node/database_utility.ts
+++ b/ts/node/database_utility.ts
@@ -264,7 +264,7 @@ export function rebuildFtsTable(db: BetterSqlite3.Database) {
           CREATE TRIGGER messages_on_delete AFTER DELETE ON ${MESSAGES_TABLE} BEGIN
             DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id;
           END;
-          CREATE TRIGGER messages_on_update AFTER UPDATE ON ${MESSAGES_TABLE} BEGIN
+          CREATE TRIGGER messages_on_update AFTER UPDATE ON ${MESSAGES_TABLE} WHEN new.body <> old.body BEGIN
             DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id;
             INSERT INTO ${MESSAGES_FTS_TABLE}(
               id,
diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts
index f6574f141..8c2ed8b40 100644
--- a/ts/node/migration/sessionMigrations.ts
+++ b/ts/node/migration/sessionMigrations.ts
@@ -78,6 +78,7 @@ const LOKI_SCHEMA_VERSIONS = [
   updateToSessionSchemaVersion26,
   updateToSessionSchemaVersion27,
   updateToSessionSchemaVersion28,
+  updateToSessionSchemaVersion29,
 ];
 
 function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@@ -1181,6 +1182,28 @@ function updateToSessionSchemaVersion28(currentVersion: number, db: BetterSqlite
   console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
 }
 
+function updateToSessionSchemaVersion29(currentVersion: number, db: BetterSqlite3.Database) {
+  const targetVersion = 29;
+  if (currentVersion >= targetVersion) {
+    return;
+  }
+
+  console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
+
+  db.transaction(() => {
+    dropFtsAndTriggers(db);
+    db.exec(`CREATE INDEX messages_unread_by_conversation ON ${MESSAGES_TABLE} (
+      unread,
+      conversationId
+    );`);
+    rebuildFtsTable(db);
+    // Keeping this empty migration because some people updated to this already, even if it is not needed anymore
+    writeSessionSchemaVersion(targetVersion, db);
+  })();
+
+  console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
+}
+
 // function printTableColumns(table: string, db: BetterSqlite3.Database) {
 //   console.info(db.pragma(`table_info('${table}');`));
 // }
diff --git a/ts/node/sql.ts b/ts/node/sql.ts
index f7270e7f7..33ae61980 100644
--- a/ts/node/sql.ts
+++ b/ts/node/sql.ts
@@ -1117,18 +1117,23 @@ function getUnreadByConversation(conversationId: string) {
  * Warning: This does not start expiration timer
  */
 function markAllAsReadByConversationNoExpiration(
-  conversationId: string
-): Array<{ id: string; timestamp: number }> {
-  const messagesUnreadBefore = assertGlobalInstance()
-    .prepare(
-      `SELECT json FROM ${MESSAGES_TABLE} WHERE
-      unread = $unread AND
-      conversationId = $conversationId;`
-    )
-    .all({
-      unread: 1,
-      conversationId,
-    });
+  conversationId: string,
+  returnMessagesUpdated: boolean
+): Array<number> {
+  let toReturn: Array<number> = [];
+  if (returnMessagesUpdated) {
+    const messagesUnreadBefore = assertGlobalInstance()
+      .prepare(
+        `SELECT json FROM ${MESSAGES_TABLE} WHERE
+  unread = $unread AND
+  conversationId = $conversationId;`
+      )
+      .all({
+        unread: 1,
+        conversationId,
+      });
+    toReturn = compact(messagesUnreadBefore.map(row => jsonToObject(row.json).sent_at));
+  }
 
   assertGlobalInstance()
     .prepare(
@@ -1142,7 +1147,7 @@ function markAllAsReadByConversationNoExpiration(
       conversationId,
     });
 
-  return compact(messagesUnreadBefore.map(row => jsonToObject(row.json).sent_at));
+  return toReturn;
 }
 
 function getUnreadCountByConversation(conversationId: string) {
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 3d4614d4b..6c2dbd28c 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -699,11 +699,23 @@ const conversationsSlice = createSlice({
         return state;
       }
 
+      let updatedMessages = state.messages;
+
+      // if some are unread, mark them as read
+      if (state.messages.some(m => m.propsForMessage.isUnread)) {
+        updatedMessages = state.messages.map(m => ({
+          ...m,
+          propsForMessage: { ...m.propsForMessage, isUnread: false },
+        }));
+      }
+
       // keep the unread visible just like in other apps. It will be shown until the user changes convo
       return {
         ...state,
         shouldHighlightMessage: false,
         firstUnreadMessageId: undefined,
+
+        messages: updatedMessages,
       };
     },
     /**