diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 35e26b3f4..9c76d2416 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -70,9 +70,10 @@
   "show": "Show",
   "sessionMessenger": "Session",
   "noSearchResults": "No results found for \"$searchTerm$\"",
-  "conversationsHeader": "Contacts and Groups",
+  "conversationsHeader": "Contacts and Groups: $count$",
   "contactsHeader": "Contacts",
   "messagesHeader": "Conversations",
+  "searchMessagesHeader": "Messages: $count$",
   "settingsHeader": "Settings",
   "typingAlt": "Typing animation for this conversation",
   "contactAvatarAlt": "Avatar for contact $name$",
diff --git a/package.json b/package.json
index 514b4e3a8..68aa9af6f 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
   },
   "scripts": {
     "start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod$MULTI electron .",
+    "start-dev": "cross-env NODE_ENV=development NODE_APP_INSTANCE=devprod$MULTI electron .",
     "build-everything": "yarn clean && yarn protobuf && yarn update-git-info && yarn sass && tsc && yarn build:workers",
     "build-everything:watch": "yarn clean && yarn protobuf && yarn update-git-info && yarn sass && yarn build:workers && tsc -w",
     "build:workers": "yarn worker:utils && yarn worker:libsession",
@@ -74,6 +75,7 @@
     "@emoji-mart/data": "^1.0.6",
     "@emoji-mart/react": "^1.0.1",
     "@reduxjs/toolkit": "1.8.5",
+    "@types/react-mentions": "^4.1.8",
     "abort-controller": "3.0.0",
     "auto-bind": "^4.0.0",
     "backbone": "1.3.3",
@@ -100,7 +102,7 @@
     "glob": "7.1.2",
     "image-type": "^4.1.0",
     "ip2country": "1.0.1",
-    "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz",
+    "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz",
     "libsodium-wrappers-sumo": "^0.7.9",
     "linkify-it": "3.0.2",
     "lodash": "^4.17.21",
@@ -121,7 +123,7 @@
     "react-draggable": "^4.4.4",
     "react-h5-audio-player": "^3.2.0",
     "react-intersection-observer": "^8.30.3",
-    "react-mentions": "^4.2.0",
+    "react-mentions": "^4.4.9",
     "react-portal": "^4.2.0",
     "react-qr-svg": "^2.2.1",
     "react-redux": "8.0.4",
@@ -168,7 +170,6 @@
     "@types/rc-slider": "^8.6.5",
     "@types/react": "^17.0.2",
     "@types/react-dom": "^17.0.2",
-    "@types/react-mentions": "^4.1.1",
     "@types/react-mic": "^12.4.1",
     "@types/react-portal": "^4.0.2",
     "@types/react-redux": "^7.1.24",
diff --git a/password_preload.js b/password_preload.js
index 18e3aad60..7463ad9b0 100644
--- a/password_preload.js
+++ b/password_preload.js
@@ -20,14 +20,6 @@ window.getEnvironment = () => config.environment;
 window.getVersion = () => config.version;
 window.getAppInstance = () => config.appInstance;
 
-const { SessionPasswordPrompt } = require('./ts/components/SessionPasswordPrompt');
-
-window.Signal = {
-  Components: {
-    SessionPasswordPrompt,
-  },
-};
-
 window.clearLocalData = async () => {
   window.log.info('reset database');
   ipcRenderer.send('resetDatabase');
diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx
index fccacbddf..efadb4958 100644
--- a/ts/components/conversation/SessionMessagesListContainer.tsx
+++ b/ts/components/conversation/SessionMessagesListContainer.tsx
@@ -17,12 +17,10 @@ import {
 import { StateType } from '../../state/reducer';
 import {
   getQuotedMessageToAnimate,
+  getSelectedConversation,
   getSortedMessagesOfSelectedConversation,
 } from '../../state/selectors/conversations';
-import {
-  getSelectedConversation,
-  getSelectedConversationKey,
-} from '../../state/selectors/selectedConversation';
+import { getSelectedConversationKey } from '../../state/selectors/selectedConversation';
 import { SessionMessagesList } from './SessionMessagesList';
 import { TypingBubble } from './TypingBubble';
 import { ConversationMessageRequestButtons } from './MessageRequestButtons';
diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx
index 7a0547e5e..a01548740 100644
--- a/ts/components/conversation/composition/CompositionBox.tsx
+++ b/ts/components/conversation/composition/CompositionBox.tsx
@@ -31,7 +31,11 @@ import { ToastUtils } from '../../../session/utils';
 import { ReduxConversationType } from '../../../state/ducks/conversations';
 import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
 import { StateType } from '../../../state/reducer';
-import { getMentionsInput, getQuotedMessage } from '../../../state/selectors/conversations';
+import {
+  getMentionsInput,
+  getQuotedMessage,
+  getSelectedConversation,
+} from '../../../state/selectors/conversations';
 import { AttachmentUtil } from '../../../util';
 import { Flex } from '../../basic/Flex';
 import { CaptionEditor } from '../../CaptionEditor';
@@ -53,7 +57,6 @@ import styled from 'styled-components';
 import { FixedBaseEmoji } from '../../../types/Reaction';
 import {
   getSelectedCanWrite,
-  getSelectedConversation,
   getSelectedConversationKey,
 } from '../../../state/selectors/selectedConversation';
 import { SettingsKey } from '../../../data/settings-key';
diff --git a/ts/components/leftpane/LeftPane.tsx b/ts/components/leftpane/LeftPane.tsx
index f113fa594..e02c069a6 100644
--- a/ts/components/leftpane/LeftPane.tsx
+++ b/ts/components/leftpane/LeftPane.tsx
@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
 import styled from 'styled-components';
 import { SectionType } from '../../state/ducks/section';
 import { getLeftPaneConversationIds } from '../../state/selectors/conversations';
-import { getSearchResultsIdsOnly, isSearching } from '../../state/selectors/search';
+import { getHasSearchResults } from '../../state/selectors/search';
 import { getFocusedSection, getOverlayMode } from '../../state/selectors/section';
 import { SessionTheme } from '../../themes/SessionTheme';
 import { SessionToastContainer } from '../SessionToastContainer';
@@ -22,18 +22,14 @@ const StyledLeftPane = styled.div`
 `;
 
 const InnerLeftPaneMessageSection = () => {
-  const showSearch = useSelector(isSearching);
-
-  const searchResults = useSelector(getSearchResultsIdsOnly);
-
+  const hasSearchResults = useSelector(getHasSearchResults);
   const conversationIds = useSelector(getLeftPaneConversationIds);
   const overlayMode = useSelector(getOverlayMode);
 
   return (
-    // tslint:disable-next-line: use-simple-attributes
     <LeftPaneMessageSection
-      conversationIds={showSearch ? undefined : conversationIds || []}
-      searchResults={showSearch ? searchResults : undefined}
+      hasSearchResults={hasSearchResults}
+      conversationIds={conversationIds}
       overlayMode={overlayMode}
     />
   );
diff --git a/ts/components/leftpane/LeftPaneMessageSection.tsx b/ts/components/leftpane/LeftPaneMessageSection.tsx
index 98504cd85..95ef19647 100644
--- a/ts/components/leftpane/LeftPaneMessageSection.tsx
+++ b/ts/components/leftpane/LeftPaneMessageSection.tsx
@@ -1,7 +1,7 @@
 import autoBind from 'auto-bind';
 import React from 'react';
 import { AutoSizer, List, ListRowProps } from 'react-virtualized';
-import { SearchResults, SearchResultsProps } from '../search/SearchResults';
+import { SearchResults } from '../search/SearchResults';
 import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
 import { MessageRequestsBanner } from './MessageRequestsBanner';
 
@@ -21,7 +21,7 @@ import { assertUnreachable } from '../../types/sqlSharedTypes';
 
 export interface Props {
   conversationIds?: Array<string>;
-  searchResults?: SearchResultsProps;
+  hasSearchResults: boolean;
   overlayMode: OverlayMode | undefined;
 }
 
@@ -88,10 +88,10 @@ export class LeftPaneMessageSection extends React.Component<Props> {
   };
 
   public renderList(): JSX.Element {
-    const { conversationIds, searchResults } = this.props;
+    const { conversationIds, hasSearchResults } = this.props;
 
-    if (searchResults) {
-      return <SearchResults {...searchResults} />;
+    if (hasSearchResults) {
+      return <SearchResults />;
     }
 
     if (!conversationIds) {
diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx
index b53a34e69..79f0df58f 100644
--- a/ts/components/search/MessageSearchResults.tsx
+++ b/ts/components/search/MessageSearchResults.tsx
@@ -6,7 +6,7 @@ import { ContactName } from '../conversation/ContactName';
 import { Avatar, AvatarSize } from '../avatar/Avatar';
 import { Timestamp } from '../conversation/Timestamp';
 import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
-import styled from 'styled-components';
+import styled, { CSSProperties } from 'styled-components';
 import { MessageAttributes } from '../../models/messageType';
 import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
 import { UserUtils } from '../../session/utils';
@@ -172,7 +172,9 @@ const StyledTimestampContaimer = styled.div`
   color: var(--conversation-tab-text-color);
 `;
 
-export const MessageSearchResult = (props: MessageResultProps) => {
+type MessageSearchResultProps = MessageResultProps & { style: CSSProperties };
+
+export const MessageSearchResult = (props: MessageSearchResultProps) => {
   const {
     id,
     conversationId,
@@ -183,6 +185,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
     serverTimestamp,
     timestamp,
     direction,
+    style,
   } = props;
 
   /** destination is only used for search results (showing the `from:` and `to`)
@@ -210,6 +213,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
   return (
     <StyledSearchResults
       key={`div-msg-searchresult-${id}`}
+      style={style}
       role="button"
       onClick={() => {
         void openConversationToSpecificMessage({
diff --git a/ts/components/search/SearchResults.tsx b/ts/components/search/SearchResults.tsx
index 894a03759..2b39a07b1 100644
--- a/ts/components/search/SearchResults.tsx
+++ b/ts/components/search/SearchResults.tsx
@@ -1,13 +1,16 @@
 import React from 'react';
-import styled from 'styled-components';
+import styled, { CSSProperties } from 'styled-components';
 import { ConversationListItem } from '../leftpane/conversation-list-item/ConversationListItem';
-import { MessageResultProps, MessageSearchResult } from './MessageSearchResults';
-
-export type SearchResultsProps = {
-  contactsAndGroupsIds: Array<string>;
-  messages: Array<MessageResultProps>;
-  searchTerm: string;
-};
+import { MessageSearchResult } from './MessageSearchResults';
+import { AutoSizer, List } from 'react-virtualized';
+import { useSelector } from 'react-redux';
+import {
+  SearchResultsMergedListItem,
+  getHasSearchResults,
+  getSearchResultsList,
+  getSearchTerm,
+} from '../../state/selectors/search';
+import { isString } from 'lodash';
 
 const StyledSeparatorSection = styled.div`
   height: 36px;
@@ -35,37 +38,58 @@ const NoResults = styled.div`
   text-align: center;
 `;
 
-export const SearchResults = (props: SearchResultsProps) => {
-  const { contactsAndGroupsIds, messages, searchTerm } = props;
+const SectionHeader = ({ title, style }: { title: string; style: CSSProperties }) => {
+  return <StyledSeparatorSection style={style}>{title}</StyledSeparatorSection>;
+};
 
-  const haveContactsAndGroup = Boolean(contactsAndGroupsIds?.length);
-  const haveMessages = Boolean(messages?.length);
-  const noResults = !haveContactsAndGroup && !haveMessages;
+function isContact(item: SearchResultsMergedListItem): item is { contactConvoId: string } {
+  return (item as any).contactConvoId !== undefined;
+}
 
+const VirtualizedList = () => {
+  const searchResultList = useSelector(getSearchResultsList);
   return (
-    <SearchResultsContainer>
-      {noResults ? <NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults> : null}
-      {haveContactsAndGroup ? (
-        <>
-          <StyledSeparatorSection>{window.i18n('conversationsHeader')}</StyledSeparatorSection>
-          {contactsAndGroupsIds.map(conversationId => (
-            <ConversationListItem
-              conversationId={conversationId}
-              key={`search-result-convo-${conversationId}`}
-            />
-          ))}
-        </>
-      ) : null}
+    <AutoSizer>
+      {({ height, width }) => (
+        <List
+          height={height}
+          rowCount={searchResultList.length}
+          rowHeight={rowPos => {
+            return isString(searchResultList[rowPos.index]) ? 36 : 64;
+          }}
+          rowRenderer={({ index, key, style }) => {
+            const row = searchResultList[index];
+            if (!row) {
+              return null;
+            }
+            if (isString(row)) {
+              return <SectionHeader title={row} style={style} key={key} />;
+            }
+            if (isContact(row)) {
+              return (
+                <ConversationListItem conversationId={row.contactConvoId} style={style} key={key} />
+              );
+            }
+            return <MessageSearchResult style={style} key={key} {...row} />;
+          }}
+          width={width}
+          autoHeight={false}
+        />
+      )}
+    </AutoSizer>
+  );
+};
+
+export const SearchResults = () => {
+  const searchTerm = useSelector(getSearchTerm);
+  const hasSearchResults = useSelector(getHasSearchResults);
 
-      {haveMessages && (
-        <>
-          <StyledSeparatorSection>
-            {`${window.i18n('messagesHeader')}: ${messages.length}`}
-          </StyledSeparatorSection>
-          {messages.map(message => (
-            <MessageSearchResult key={`search-result-message-${message.id}`} {...message} />
-          ))}
-        </>
+  return (
+    <SearchResultsContainer>
+      {!hasSearchResults ? (
+        <NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults>
+      ) : (
+        <VirtualizedList />
       )}
     </SearchResultsContainer>
   );
diff --git a/ts/data/data.ts b/ts/data/data.ts
index d4e9ab035..6ecbfe3bd 100644
--- a/ts/data/data.ts
+++ b/ts/data/data.ts
@@ -497,27 +497,52 @@ async function getSeenMessagesByHashList(hashes: Array<string>): Promise<any> {
 }
 
 async function removeAllMessagesInConversation(conversationId: string): Promise<void> {
+  const startFunction = Date.now();
+  let start = Date.now();
+
   let messages;
   do {
     // Yes, we really want the await in the loop. We're deleting 500 at a
     //   time so we don't use too much memory.
     // eslint-disable-next-line no-await-in-loop
-    messages = await getLastMessagesByConversation(conversationId, 500, false);
+    messages = await getLastMessagesByConversation(conversationId, 1000, false);
     if (!messages.length) {
       return;
     }
-
-    const ids = messages.map(message => message.id);
+    window.log.info(
+      `removeAllMessagesInConversation getLastMessagesByConversation ${conversationId} ${
+        messages.length
+      } took ${Date.now() - start}ms`
+    );
 
     // Note: It's very important that these models are fully hydrated because
     //   we need to delete all associated on-disk files along with the database delete.
-    // eslint-disable-next-line no-await-in-loop
-
-    await Promise.all(messages.map(message => message.cleanup()));
+    const ids = messages.map(message => message.id);
+    start = Date.now();
+    for (let index = 0; index < messages.length; index++) {
+      const message = messages.at(index);
+      // eslint-disable-next-line no-await-in-loop
+      await message.cleanup();
+    }
+    window.log.info(
+      `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${Date.now() -
+        start}ms`
+    );
+    start = Date.now();
 
     // eslint-disable-next-line no-await-in-loop
     await channels.removeMessagesByIds(ids);
-  } while (messages.length > 0);
+    window.log.info(
+      `removeAllMessagesInConversation: removeMessagesByIds ${conversationId} took ${Date.now() -
+        start}ms`
+    );
+  } while (messages.length);
+
+  await channels.removeAllMessagesInConversation(conversationId);
+  window.log.info(
+    `removeAllMessagesInConversation: complete time ${conversationId} took ${Date.now() -
+      startFunction}ms`
+  );
 }
 
 async function getMessagesBySentAt(sentAt: number): Promise<MessageCollection> {
diff --git a/ts/node/database_utility.ts b/ts/node/database_utility.ts
index 428d7754b..f36f15135 100644
--- a/ts/node/database_utility.ts
+++ b/ts/node/database_utility.ts
@@ -242,29 +242,29 @@ export function rebuildFtsTable(db: BetterSqlite3.Database) {
   db.exec(`
           -- Then we create our full-text search table and populate it
           CREATE VIRTUAL TABLE ${MESSAGES_FTS_TABLE}
-            USING fts5(id UNINDEXED, body);
-          INSERT INTO ${MESSAGES_FTS_TABLE}(id, body)
-            SELECT id, body FROM ${MESSAGES_TABLE};
+            USING fts5(body);
+          INSERT INTO ${MESSAGES_FTS_TABLE}(rowid, body)
+            SELECT rowid, body FROM ${MESSAGES_TABLE};
           -- Then we set up triggers to keep the full-text search table up to date
           CREATE TRIGGER messages_on_insert AFTER INSERT ON ${MESSAGES_TABLE} BEGIN
             INSERT INTO ${MESSAGES_FTS_TABLE} (
-              id,
+              rowid,
               body
             ) VALUES (
-              new.id,
+              new.rowid,
               new.body
             );
           END;
           CREATE TRIGGER messages_on_delete AFTER DELETE ON ${MESSAGES_TABLE} BEGIN
-            DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id;
+            DELETE FROM ${MESSAGES_FTS_TABLE} WHERE rowid = old.rowid;
           END;
           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;
+            DELETE FROM ${MESSAGES_FTS_TABLE} WHERE rowid = old.rowid;
             INSERT INTO ${MESSAGES_FTS_TABLE}(
-              id,
+              rowid,
               body
             ) VALUES (
-              new.id,
+              new.rowid,
               new.body
             );
           END;
diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts
index 1471ad499..9c25bf3f4 100644
--- a/ts/node/migration/sessionMigrations.ts
+++ b/ts/node/migration/sessionMigrations.ts
@@ -33,6 +33,7 @@ import {
 } from '../database_utility';
 
 import { getIdentityKeys, sqlNode } from '../sql';
+import { sleepFor } from '../../session/utils/Promise';
 
 const hasDebugEnvVariable = Boolean(process.env.SESSION_DEBUG);
 
@@ -100,6 +101,7 @@ const LOKI_SCHEMA_VERSIONS = [
   updateToSessionSchemaVersion29,
   updateToSessionSchemaVersion30,
   updateToSessionSchemaVersion31,
+  updateToSessionSchemaVersion32,
 ];
 
 function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@@ -1204,7 +1206,6 @@ function updateToSessionSchemaVersion29(currentVersion: number, db: BetterSqlite
       conversationId
     );`);
     rebuildFtsTable(db);
-    // Keeping this empty migration because some people updated to this already, even if it is not needed anymore
     writeSessionSchemaVersion(targetVersion, db);
   })();
 
@@ -1235,6 +1236,8 @@ function insertContactIntoContactWrapper(
       dbProfileKey: contact.profileKey || undefined,
       dbProfileUrl: contact.avatarPointer || undefined,
       priority,
+      dbCreatedAtSeconds: Math.floor((contact.active_at || Date.now()) / 1000),
+
       // expirationTimerSeconds,
     });
 
@@ -1259,6 +1262,7 @@ function insertContactIntoContactWrapper(
             dbProfileKey: undefined,
             dbProfileUrl: undefined,
             priority: CONVERSATION_PRIORITIES.default,
+            dbCreatedAtSeconds: Math.floor(Date.now() / 1000),
             // expirationTimerSeconds: 0,
           })
         );
@@ -1308,7 +1312,7 @@ function insertCommunityIntoWrapper(
   const convoId = community.id; // the id of a conversation has the prefix, the serverUrl and the roomToken already present, but not the pubkey
 
   const roomDetails = sqlNode.getV2OpenGroupRoom(convoId, db);
-  hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community);
+  // hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community);
 
   if (
     !roomDetails ||
@@ -1832,6 +1836,26 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
   })();
 }
 
+function updateToSessionSchemaVersion32(currentVersion: number, db: BetterSqlite3.Database) {
+  const targetVersion = 32;
+  if (currentVersion >= targetVersion) {
+    return;
+  }
+
+  console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
+
+  db.transaction(() => {
+    db.exec(`CREATE INDEX messages_conversationId ON ${MESSAGES_TABLE} (
+      conversationId
+    );`);
+    dropFtsAndTriggers(db);
+    rebuildFtsTable(db);
+    writeSessionSchemaVersion(targetVersion, db);
+  })();
+
+  console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
+}
+
 export function printTableColumns(table: string, db: BetterSqlite3.Database) {
   console.info(db.pragma(`table_info('${table}');`));
 }
@@ -1846,7 +1870,7 @@ function writeSessionSchemaVersion(newVersion: number, db: BetterSqlite3.Databas
   ).run({ newVersion });
 }
 
-export function updateSessionSchema(db: BetterSqlite3.Database) {
+export async function updateSessionSchema(db: BetterSqlite3.Database) {
   const result = db
     .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';`)
     .get();
@@ -1863,5 +1887,8 @@ export function updateSessionSchema(db: BetterSqlite3.Database) {
   for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) {
     const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index];
     runSchemaUpdate(lokiSchemaVersion, db);
+    if (index > lokiSchemaVersion) {
+      await sleepFor(200); // give some time for the UI to not freeze between 2 migrations
+    }
   }
 }
diff --git a/ts/node/migration/signalMigrations.ts b/ts/node/migration/signalMigrations.ts
index 0c23a7258..ff4caed75 100644
--- a/ts/node/migration/signalMigrations.ts
+++ b/ts/node/migration/signalMigrations.ts
@@ -527,7 +527,7 @@ const SCHEMA_VERSIONS = [
   updateToSchemaVersion11,
 ];
 
-export function updateSchema(db: BetterSqlite3.Database) {
+export async function updateSchema(db: BetterSqlite3.Database) {
   const sqliteVersion = getSQLiteVersion(db);
   const sqlcipherVersion = getSQLCipherVersion(db);
   const userVersion = getUserVersion(db);
@@ -545,7 +545,7 @@ export function updateSchema(db: BetterSqlite3.Database) {
     const runSchemaUpdate = SCHEMA_VERSIONS[index];
     runSchemaUpdate(schemaVersion, db);
   }
-  updateSessionSchema(db);
+  await updateSessionSchema(db);
 }
 
 function migrateSchemaVersion(db: BetterSqlite3.Database) {
diff --git a/ts/node/sql.ts b/ts/node/sql.ts
index 863f29e6e..47d40da65 100644
--- a/ts/node/sql.ts
+++ b/ts/node/sql.ts
@@ -28,7 +28,6 @@ import {
   ATTACHMENT_DOWNLOADS_TABLE,
   CLOSED_GROUP_V2_KEY_PAIRS_TABLE,
   CONVERSATIONS_TABLE,
-  dropFtsAndTriggers,
   formatRowOfConversation,
   GUARD_NODE_TABLE,
   HEX_KEY,
@@ -41,7 +40,6 @@ import {
   NODES_FOR_PUBKEY_TABLE,
   objectToJSON,
   OPEN_GROUP_ROOMS_V2_TABLE,
-  rebuildFtsTable,
   toSqliteBoolean,
 } from './database_utility';
 import { LocaleMessagesType } from './locale'; // checked - only node
@@ -166,7 +164,7 @@ async function initializeSql({
     if (!db) {
       throw new Error('db is not set');
     }
-    updateSchema(db);
+    await updateSchema(db);
 
     // test database
 
@@ -698,9 +696,9 @@ function searchMessages(query: string, limit: number) {
       ${MESSAGES_TABLE}.json,
       snippet(${MESSAGES_FTS_TABLE}, -1, '<<left>>', '<<right>>', '...', 5) as snippet
     FROM ${MESSAGES_FTS_TABLE}
-    INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.id = ${MESSAGES_TABLE}.id
+    INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.rowid = ${MESSAGES_TABLE}.rowid
     WHERE
-     ${MESSAGES_FTS_TABLE} match $query
+     ${MESSAGES_FTS_TABLE}.body match $query
     ${orderByMessageCoalesceClause}
     LIMIT $limit;`
     )
@@ -958,11 +956,12 @@ function removeMessagesByIds(ids: Array<string>, instance?: BetterSqlite3.Databa
   if (!ids.length) {
     throw new Error('removeMessagesByIds: No ids to delete!');
   }
+  const start = Date.now();
 
-  // Our node interface doesn't seem to allow you to replace one single ? with an array
   assertGlobalInstanceOrInstance(instance)
     .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id IN ( ${ids.map(() => '?').join(', ')} );`)
     .run(ids);
+  console.log(`removeMessagesByIds of length ${ids.length} took ${Date.now() - start}ms`);
 }
 
 function removeAllMessagesInConversation(
@@ -972,9 +971,9 @@ function removeAllMessagesInConversation(
   if (!conversationId) {
     return;
   }
+  const inst = assertGlobalInstanceOrInstance(instance);
 
-  // Our node interface doesn't seem to allow you to replace one single ? with an array
-  assertGlobalInstanceOrInstance(instance)
+  inst
     .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`)
     .run({ conversationId });
 }
@@ -2233,7 +2232,6 @@ function cleanUpOldOpengroupsOnStart() {
   // first remove very old messages for each opengroups
   const db = assertGlobalInstance();
   db.transaction(() => {
-    dropFtsAndTriggers(db);
     v2ConvosIds.forEach(convoId => {
       const messagesInConvoBefore = getMessagesCountByConversation(convoId);
 
@@ -2316,8 +2314,6 @@ function cleanUpOldOpengroupsOnStart() {
     }
 
     cleanUpMessagesJson();
-
-    rebuildFtsTable(db);
   })();
 }
 
diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts
index f50135a14..0cef20c86 100644
--- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts
+++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts
@@ -294,7 +294,7 @@ async function queueNewJobIfNeeded() {
     !lastRunConfigSyncJobTimestamp ||
     lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries
   ) {
-    window.log.debug('Scheduling ConfSyncJob: ASAP');
+    // window.log.debug('Scheduling ConfSyncJob: ASAP');
     // we postpone by 1000ms to make sure whoever is adding this job is done with what is needs to do first
     // this call will make sure that there is only one configuration sync job at all times
     await runners.configurationSyncRunner.addJob(
@@ -305,7 +305,7 @@ async function queueNewJobIfNeeded() {
     const diff = Math.max(Date.now() - lastRunConfigSyncJobTimestamp, 0);
     // but we want to run every 30, so what we need is actually `30-10` from now = 20
     const leftBeforeNextTick = Math.max(defaultMsBetweenRetries - diff, 1000);
-    window.log.debug('Scheduling ConfSyncJob: LATER');
+    // window.log.debug('Scheduling ConfSyncJob: LATER');
 
     await runners.configurationSyncRunner.addJob(
       new ConfigurationSyncJob({ nextAttemptTimestamp: Date.now() + leftBeforeNextTick })
diff --git a/ts/session/utils/libsession/libsession_utils_contacts.ts b/ts/session/utils/libsession/libsession_utils_contacts.ts
index a960352ae..a5d16d5df 100644
--- a/ts/session/utils/libsession/libsession_utils_contacts.ts
+++ b/ts/session/utils/libsession/libsession_utils_contacts.ts
@@ -70,6 +70,7 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
     dbProfileKey,
     dbProfileUrl,
     priority,
+    dbCreatedAtSeconds: 0, // just give 0, now() will be used internally by the wrapper if the contact does not exist yet.
     // expirationTimerSeconds,
   });
   try {
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index f9d4245e2..c9c44fa9e 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -31,7 +31,7 @@ import { getIntl } from './user';
 
 import { filter, isEmpty, isNumber, pick, sortBy } from 'lodash';
 import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
-import { getSelectedConversation, getSelectedConversationKey } from './selectedConversation';
+import { getSelectedConversationKey } from './selectedConversation';
 import { getModeratorsOutsideRedux } from './sogsRoomInfo';
 
 export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@@ -626,6 +626,17 @@ export const isFirstUnreadMessageIdAbove = createSelector(
 
 const getMessageId = (_whatever: any, id: string | undefined) => id;
 
+/**
+ * A lot of our UI changes on the main panel need to happen quickly (composition box).
+ */
+export const getSelectedConversation = createSelector(
+  getConversationLookup,
+  getSelectedConversationKey,
+  (lookup, selectedConvo) => {
+    return selectedConvo ? lookup[selectedConvo] : undefined;
+  }
+);
+
 // tslint:disable: cyclomatic-complexity
 
 export const getMessagePropsByMessageId = createSelector(
diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts
index b05e06cbf..6238742b8 100644
--- a/ts/state/selectors/search.ts
+++ b/ts/state/selectors/search.ts
@@ -1,11 +1,12 @@
 import { createSelector } from '@reduxjs/toolkit';
-import { compact } from 'lodash';
+import { compact, isEmpty } from 'lodash';
 
 import { StateType } from '../reducer';
 
 import { ConversationLookupType } from '../ducks/conversations';
 import { SearchStateType } from '../ducks/search';
 import { getConversationLookup } from './conversations';
+import { MessageResultProps } from '../../components/search/MessageSearchResults';
 
 export const getSearch = (state: StateType): SearchStateType => state.search;
 
@@ -41,6 +42,10 @@ const getSearchResults = createSelector(
   }
 );
 
+export const getSearchTerm = createSelector([getSearchResults], searchResult => {
+  return searchResult.searchTerm;
+});
+
 export const getSearchResultsIdsOnly = createSelector([getSearchResults], searchState => {
   return {
     ...searchState,
@@ -48,6 +53,32 @@ export const getSearchResultsIdsOnly = createSelector([getSearchResults], search
   };
 });
 
+export const getHasSearchResults = createSelector([getSearchResults], searchState => {
+  return !isEmpty(searchState.contactsAndGroups) || !isEmpty(searchState.messages);
+});
+
 export const getSearchResultsContactOnly = createSelector([getSearchResults], searchState => {
   return searchState.contactsAndGroups.filter(m => m.isPrivate).map(m => m.id);
 });
+
+/**
+ *
+ * When type is string, we render a sectionHeader.
+ * When type just has a conversationId field, we render a ConversationListItem.
+ * When type is MessageResultProps we render a MessageSearchResult
+ */
+export type SearchResultsMergedListItem = string | { contactConvoId: string } | MessageResultProps;
+
+export const getSearchResultsList = createSelector([getSearchResults], searchState => {
+  const { contactsAndGroups, messages } = searchState;
+  const builtList: Array<SearchResultsMergedListItem> = [];
+  if (contactsAndGroups.length) {
+    builtList.push(window.i18n('conversationsHeader', [`${contactsAndGroups.length}`]));
+    builtList.push(...contactsAndGroups.map(m => ({ contactConvoId: m.id })));
+  }
+  if (messages.length) {
+    builtList.push(window.i18n('searchMessagesHeader', [`${messages.length}`]));
+    builtList.push(...messages);
+  }
+  return builtList;
+});
diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts
index a1e186d9c..31523151f 100644
--- a/ts/state/selectors/selectedConversation.ts
+++ b/ts/state/selectors/selectedConversation.ts
@@ -3,9 +3,9 @@ import { useSelector } from 'react-redux';
 import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes';
 import { PubKey } from '../../session/types';
 import { UserUtils } from '../../session/utils';
-import { ReduxConversationType } from '../ducks/conversations';
 import { StateType } from '../reducer';
 import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo';
+import { getSelectedConversation } from './conversations';
 
 /**
  * Returns the formatted text for notification setting.
@@ -58,11 +58,6 @@ export const getSelectedConversationKey = (state: StateType): string | undefined
   return state.conversations.selectedConversation;
 };
 
-export const getSelectedConversation = (state: StateType): ReduxConversationType | undefined => {
-  const selected = getSelectedConversationKey(state);
-  return selected ? state.conversations.conversationLookup[selected] : undefined;
-};
-
 /**
  * Returns true if the current conversation selected is a public group and false otherwise.
  */
diff --git a/ts/state/smart/SessionConversation.ts b/ts/state/smart/SessionConversation.ts
index c5850f2f1..6b40c2549 100644
--- a/ts/state/smart/SessionConversation.ts
+++ b/ts/state/smart/SessionConversation.ts
@@ -6,15 +6,13 @@ import { getHasOngoingCallWithFocusedConvo } from '../selectors/call';
 import {
   getIsSelectedConvoInitialLoadingInProgress,
   getLightBoxOptions,
+  getSelectedConversation,
   getSelectedMessageIds,
   getSortedMessagesOfSelectedConversation,
   isMessageDetailView,
   isRightPanelShowing,
 } from '../selectors/conversations';
-import {
-  getSelectedConversation,
-  getSelectedConversationKey,
-} from '../selectors/selectedConversation';
+import { getSelectedConversationKey } from '../selectors/selectedConversation';
 import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
 import { getTheme } from '../selectors/theme';
 import { getOurNumber } from '../selectors/user';
diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts
index 1c7f7a0e0..7401354a6 100644
--- a/ts/types/LocalizerKeys.ts
+++ b/ts/types/LocalizerKeys.ts
@@ -73,6 +73,7 @@ export type LocalizerKeys =
   | 'conversationsHeader'
   | 'contactsHeader'
   | 'messagesHeader'
+  | 'searchMessagesHeader'
   | 'settingsHeader'
   | 'typingAlt'
   | 'contactAvatarAlt'
diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts
index 56b976c35..a3d3cf5f8 100644
--- a/ts/types/sqlSharedTypes.ts
+++ b/ts/types/sqlSharedTypes.ts
@@ -115,6 +115,7 @@ export function getContactInfoFromDBValues({
   priority,
   dbProfileUrl,
   dbProfileKey,
+  dbCreatedAtSeconds,
 }: // expirationTimerSeconds,
 {
   id: string;
@@ -124,6 +125,7 @@ export function getContactInfoFromDBValues({
   dbNickname: string | undefined;
   dbName: string | undefined;
   priority: number;
+  dbCreatedAtSeconds: number;
   dbProfileUrl: string | undefined;
   dbProfileKey: string | undefined;
   // expirationTimerSeconds: number | undefined;
@@ -136,6 +138,7 @@ export function getContactInfoFromDBValues({
     priority,
     nickname: dbNickname,
     name: dbName,
+    createdAtSeconds: dbCreatedAtSeconds,
     // expirationTimerSeconds:
     //   !!expirationTimerSeconds && isFinite(expirationTimerSeconds) && expirationTimerSeconds > 0
     //     ? expirationTimerSeconds
diff --git a/yarn.lock b/yarn.lock
index 84fe9eff7..2b8c5284f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -411,13 +411,20 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
   version "7.19.0"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
   integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.3.4":
+  version "7.22.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
+  integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
 "@babel/template@^7.18.6":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
@@ -1192,10 +1199,10 @@
   dependencies:
     "@types/react" "^17"
 
-"@types/react-mentions@^4.1.1":
-  version "4.1.6"
-  resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.6.tgz#0ecdb61785c22edbf9c7d6718505d4814ad3a65c"
-  integrity sha512-f4/BdnjlMxT47q+WqlcYYwFABbBMVQrDoFFeMeljtFC5nnR9/x8TOFmN18BJKgNuWMgivy9uE5EKtsjlay751w==
+"@types/react-mentions@^4.1.8":
+  version "4.1.8"
+  resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.8.tgz#4bebe54c5c74181d8eedf1e613a208d03b4a8d7e"
+  integrity sha512-Go86ozdnh0FTNbiGiDPAcNqYqtab9iGzLOgZPYUKrnhI4539jGzfJtP6rFHcXgi9Koe58yhkeyKYib6Ucul/sQ==
   dependencies:
     "@types/react" "*"
 
@@ -5148,9 +5155,9 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz":
-  version "0.1.19"
-  resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz#294c6e8ea6b767d375a9c0249bef98b65f3ae252"
+"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz":
+  version "0.1.20"
+  resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz#4ff8331e4efb1725cf8bba0ef1af7496ebd8533d"
   dependencies:
     cmake-js "^7.2.1"
     node-addon-api "^6.1.0"
@@ -6551,10 +6558,10 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
-react-mentions@^4.2.0:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.3.2.tgz#93a2a674648f31d2b40e8c90d30b94c377cbb582"
-  integrity sha512-NV8ixuE5W9zuvBNWLpPlO+f4QYEkR+p6mR3Jfpfcbytrqqn2nbVb27YXE/M4qSP8N8C+ktgeMUV4jVhm86gt1A==
+react-mentions@^4.4.9:
+  version "4.4.9"
+  resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.4.9.tgz#5f68c7978c107518646c5c34c47515c30259e23a"
+  integrity sha512-CUDt8GOVbAmo3o+a8l1UxcJ/gJMdFdqeiJM3U5+krcNoUwyKv7Zcy67WfFZQJfChpJ8LTiD0FtCSRoyEzC6Ysw==
   dependencies:
     "@babel/runtime" "7.4.5"
     invariant "^2.2.4"
@@ -6759,7 +6766,12 @@ regenerator-runtime@^0.11.0:
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
-regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4:
+regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2:
+  version "0.13.11"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+regenerator-runtime@^0.13.4:
   version "0.13.9"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
   integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==