diff --git a/css/private_message_slide.css b/css/private_message_slide.css
new file mode 100644
index 0000000000000000000000000000000000000000..f6380e1d231e9e79c4ad6cf9b3fa3584fffccf45
--- /dev/null
+++ b/css/private_message_slide.css
@@ -0,0 +1,14 @@
+.private-message-slide {
+  overflow: hidden;
+  transition:
+    height 300ms ease-out,
+    opacity 300ms ease-out;
+}
+.private-message-slide__collapsed {
+  height: 0;
+  opacity: 0;
+}
+.private-message-slide__expanded {
+  height: auto;
+  opacity: 1;
+}
diff --git a/css/private_message_slide.scss b/css/private_message_slide.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3fcc7df311204f2054de6307719d0ea3961ae298
--- /dev/null
+++ b/css/private_message_slide.scss
@@ -0,0 +1,17 @@
+.private-message-slide {
+  overflow: hidden;
+  transition:
+    height 300ms ease-out,
+    opacity 300ms ease-out;
+
+  &__collapsed {
+    height: 0;
+    opacity: 0;
+  }
+
+  &__expanded {
+    height: auto;
+    opacity: 1;
+  }
+
+}
diff --git a/js/private_message_inbox_block.js b/js/private_message_inbox_block.js
index 7516f2950d5d68080b62480f1bf8bbe3a08c7b90..e36d9aa7569d5bcf8ff7f25ce797a09fb3a24f8a 100644
--- a/js/private_message_inbox_block.js
+++ b/js/private_message_inbox_block.js
@@ -3,271 +3,253 @@
  * Adds JavaScript functionality to the private message inbox block.
  */
 
-Drupal.PrivateMessageInbox = {};
-Drupal.PrivateMessageInbox.updateInbox = {};
-
-(($, Drupal, drupalSettings, window, once) => {
-  let container;
-  let updateInterval;
-  let loadingPrev;
-  let loadingNew;
-
+((Drupal, drupalSettings, window, once) => {
   /**
-   * Used to manually trigger Drupal's JavaScript commands.
-   * @param {Object} data The data.
+   * Private message inbox block functionality.
    */
-  function triggerCommands(data) {
-    const ajaxObject = Drupal.ajax({
-      url: '',
-      base: false,
-      element: false,
-      progress: false,
-    });
+  class PrivateMessageInboxBlock {
+    constructor() {
+      this.container = null;
+      this.loadingPrevInProgress = false;
+      this.loadingNewInProgress = false;
+      this.updateTimeoutId = null;
+    }
 
-    // Trigger any ajax commands in the response.
-    ajaxObject.success(data, 'success');
-  }
+    /**
+     * Initialize the block with default state and handlers.
+     *
+     * @param {HTMLElement} blockWrapper
+     *   The inbox block.
+     */
+    init(blockWrapper) {
+      this.container = blockWrapper;
+      const threadId = this.container.querySelector('.private-message-thread')
+        ?.dataset.threadId;
+      if (threadId) {
+        Drupal.PrivateMessageUtils.setActiveThread(threadId);
+      }
+      this.attachLoadOldButton();
+      this.scheduleInboxUpdate();
+    }
 
-  /**
-   * Updates the inbox after an Ajax call.
-   */
-  function updateInbox() {
-    if (!loadingNew) {
-      loadingNew = true;
+    /**
+     * Updates the inbox with new threads.
+     */
+    updateInbox() {
+      if (this.loadingNewInProgress) {
+        return;
+      }
 
+      this.loadingNewInProgress = true;
       const ids = {};
 
-      if (container.length > 0) {
-        container[0]
-          .querySelectorAll('.private-message-thread-inbox')
-          .forEach((el) => {
-            ids[el.dataset.threadId] = el.dataset.lastUpdate;
-          });
-      }
+      this.container
+        .querySelectorAll('.private-message-thread-inbox')
+        .forEach((el) => {
+          ids[el.dataset.threadId] = el.dataset.lastUpdate;
+        });
 
-      $.ajax({
+      Drupal.ajax({
         url: drupalSettings.privateMessageInboxBlock.loadNewUrl,
-        method: 'POST',
-        data: { ids },
-        success(data) {
-          loadingNew = false;
-          triggerCommands(data);
-          if (updateInterval) {
-            window.setTimeout(updateInbox, updateInterval);
-          }
-        },
-      });
+        submit: { ids },
+      })
+        .execute()
+        .always(() => {
+          this.loadingNewInProgress = false;
+          this.scheduleInboxUpdate();
+        });
     }
-  }
-
-  /**
-   * Reorders the inbox after an Ajax Load, to show newest threads first.
-   * @param {Array} threadIds The threads IDs.
-   * @param {Array} newThreads The new Threads.
-   */
-  function reorderInbox(threadIds, newThreads) {
-    const map = {};
-
-    container[0]
-      .querySelectorAll(':scope > .private-message-thread-inbox')
-      .forEach((el) => {
-        map[el.dataset.threadId] = $(el);
-      });
-
-    threadIds.forEach((threadId) => {
-      if (newThreads[threadId]) {
-        if (map[threadId]) {
-          map[threadId].remove();
-        }
 
-        $('<div/>').html(newThreads[threadId]).contents().appendTo(container);
-      } else if (map[threadId]) {
-        container.append(map[threadId]);
+    /**
+     * Sets a timeout for inbox updates.
+     */
+    scheduleInboxUpdate() {
+      if (this.updateTimeoutId) {
+        window.clearTimeout(this.updateTimeoutId);
       }
-    });
-
-    Drupal.attachBehaviors(container[0]);
-  }
+      const interval =
+        drupalSettings.privateMessageInboxBlock.ajaxRefreshRate * 1000;
+      if (interval) {
+        this.updateTimeoutId = window.setTimeout(
+          () => this.updateInbox(),
+          interval,
+        );
+      }
+    }
 
-  /**
-   * Inserts older threads into the inbox after an Ajax load.
-   * @param {string} threads The threads HTML.
-   */
-  function insertPreviousThreads(threads) {
-    const contents = $('<div/>').html(threads).contents();
+    /**
+     * Appends older threads to the inbox.
+     *
+     * @param {string} threadsHtml
+     *   HTML content of threads.
+     */
+    insertPreviousThreads(threadsHtml) {
+      const newNodes = Drupal.PrivateMessageUtils.parseHTML(threadsHtml);
 
-    contents.css('display', 'none').appendTo(container).slideDown(300);
-    Drupal.attachBehaviors(contents[0]);
-  }
+      Array.from(newNodes).forEach((node) => {
+        const appendedElement = this.container.appendChild(node);
 
-  /**
-   * Adds CSS classes to the currently selected thread.
-   * @param {string} threadId The thread id.
-   */
-  function setActiveThread(threadId) {
-    container.find('.active-thread:first').removeClass('active-thread');
-    container
-      .find(`.private-message-thread[data-thread-id="${threadId}"]:first`)
-      .removeClass('unread-thread')
-      .addClass('active-thread');
-  }
+        Drupal.attachBehaviors(appendedElement);
+        Drupal.PrivateMessageSlide.toggleSlide(appendedElement, true);
+      });
+    }
 
-  /**
-   * Click handler for the button that loads older threads into the inbox.
-   * @param {Object} e The event.
-   */
-  function loadOldThreadWatcherHandler(e) {
-    e.preventDefault();
+    /**
+     * Handles loading older threads.
+     *
+     * @param {Event} e
+     *   The click event.
+     */
+    loadOldThreads(e) {
+      e.preventDefault();
+      if (this.loadingPrevInProgress) {
+        return;
+      }
 
-    if (!loadingPrev) {
-      loadingPrev = true;
+      this.loadingPrevInProgress = true;
 
-      let oldestTimestamp;
-      container[0].querySelectorAll('.private-message-thread').forEach((el) => {
-        const timestamp = parseInt(el.dataset.lastUpdate, 10);
-        oldestTimestamp = !oldestTimestamp
-          ? timestamp
-          : Math.min(timestamp, oldestTimestamp);
-      });
+      const oldestTimestamp = Array.from(
+        this.container.querySelectorAll('.private-message-thread'),
+      ).reduce((minTime, el) => {
+        return Math.min(minTime, parseInt(el.dataset.lastUpdate, 10));
+      }, Infinity);
 
-      $.ajax({
+      Drupal.ajax({
         url: drupalSettings.privateMessageInboxBlock.loadPrevUrl,
-        data: {
+        submit: {
           timestamp: oldestTimestamp,
           count: drupalSettings.privateMessageInboxBlock.threadCount,
         },
-        success(data) {
-          loadingPrev = false;
-          triggerCommands(data);
-        },
-      });
+      })
+        .execute()
+        .always(() => {
+          this.loadingPrevInProgress = false;
+        });
     }
-  }
 
-  /**
-   * Watches the button that loads previous threads into the inbox.
-   * @param {Object} context The context.
-   */
-  function loadOlderThreadWatcher(context) {
-    once(
-      'load-older-threads-watcher',
-      '#load-previous-threads-button',
-      context,
-    ).forEach((el) => {
-      el.addEventListener('click', loadOldThreadWatcherHandler);
-    });
-  }
-
-  /**
-   * Click Handler executed when private message threads are clicked.
-   *
-   * Loads the thread into the private message window.
-   * @param {Event} e The event.
-   */
-  const inboxThreadLinkListenerHandler = (e) => {
-    if (Drupal.PrivateMessages) {
-      e.preventDefault();
-      const threadId = e.currentTarget.dataset.threadId;
-      Drupal.PrivateMessages.loadThread(threadId);
-      setActiveThread(threadId);
+    /**
+     * Reorders the inbox to show the newest threads first.
+     *
+     * @param {Array} threadIds
+     *   Thread IDs in the desired order.
+     * @param {Array} newThreads
+     *   HTML content of new threads keyed by thread ID.
+     */
+    reorderInbox(threadIds, newThreads) {
+      const existingThreads = {};
+
+      this.container
+        .querySelectorAll(':scope > .private-message-thread-inbox')
+        .forEach((el) => {
+          existingThreads[el.dataset.threadId] = el;
+        });
+
+      threadIds.forEach((threadId) => {
+        if (newThreads[threadId]) {
+          if (existingThreads[threadId]) {
+            existingThreads[threadId].remove();
+          }
+          const newThreadContent = Drupal.PrivateMessageUtils.parseHTML(
+            newThreads[threadId],
+          );
+          Array.from(newThreadContent).forEach((child) => {
+            const appendedElement = this.container.appendChild(child);
+            Drupal.attachBehaviors(appendedElement);
+          });
+        } else if (existingThreads[threadId]) {
+          const appendedElement = this.container.appendChild(
+            existingThreads[threadId],
+          );
+          Drupal.attachBehaviors(appendedElement);
+        }
+      });
     }
-  };
-
-  /**
-   * Watches private message threads for clicks, so new threads can be loaded.
-   * @param {Object} context The context.
-   */
-  function inboxThreadLinkListener(context) {
-    once(
-      'inbox-thread-link-listener',
-      '.private-message-inbox-thread-link',
-      context,
-    ).forEach((el) => {
-      el.addEventListener('click', inboxThreadLinkListenerHandler);
-    });
-  }
-
-  /**
-   * Initializes the private message inbox JavaScript.
-   */
-  function init(context) {
-    $(
-      once(
-        'init-inbox-block',
-        '.block-private-message-inbox-block .private-message-thread--full-container',
-        context,
-      ),
-    ).each(function () {
-      container = $(this);
-
-      const threadId = container
-        .children('.private-message-thread:first')
-        .attr('data-thread-id');
-      setActiveThread(threadId);
 
+    /**
+     * Attaches the "Load Older Threads" button handler.
+     */
+    attachLoadOldButton() {
       if (
         drupalSettings.privateMessageInboxBlock.totalThreads >
         drupalSettings.privateMessageInboxBlock.itemsToShow
       ) {
-        $('<div/>', { id: 'load-previous-threads-button-wrapper' })
-          .append(
-            $('<a/>', { href: '#', id: 'load-previous-threads-button' }).text(
-              Drupal.t('Load Previous'),
-            ),
-          )
-          .insertAfter(container);
-        loadOlderThreadWatcher(document);
-      }
-      updateInterval =
-        drupalSettings.privateMessageInboxBlock.ajaxRefreshRate * 1000;
-      if (updateInterval) {
-        window.setTimeout(updateInbox, updateInterval);
+        Drupal.privateMessageInboxPrevious.displayButton(this.container, (e) =>
+          this.loadOldThreads(e),
+        );
       }
-    });
+    }
   }
 
+  const privateMessageInboxBlock = new PrivateMessageInboxBlock();
+
+  /**
+   * Attaches the private message inbox block behavior.
+   */
   Drupal.behaviors.privateMessageInboxBlock = {
     attach(context) {
-      init(context);
-      inboxThreadLinkListener(context);
-
-      Drupal.AjaxCommands.prototype.insertInboxOldPrivateMessageThreads = (
-        ajax,
-        response,
-      ) => {
-        if (response.threads) {
-          insertPreviousThreads(response.threads);
-        }
-        if (!response.threads || !response.hasNext) {
-          $('#load-previous-threads-button')
-            .parent()
-            .slideUp(300, function () {
-              $(this).remove();
-            });
-        }
-      };
-
-      Drupal.AjaxCommands.prototype.privateMessageInboxUpdate = (
-        ajax,
-        response,
-      ) => reorderInbox(response.threadIds, response.newThreads);
-
-      Drupal.AjaxCommands.prototype.privateMessageTriggerInboxUpdate = () =>
-        updateInbox();
+      const containerFormContext = once(
+        'private-message-inbox-block',
+        '.block-private-message-inbox-block .private-message-thread--full-container',
+        context,
+      ).shift();
 
-      if (Drupal.PrivateMessages) {
-        Drupal.PrivateMessages.setActiveThread = (id) => setActiveThread(id);
+      if (!containerFormContext) {
+        return;
       }
 
-      Drupal.PrivateMessageInbox.updateInbox = () => updateInbox();
+      privateMessageInboxBlock.init(containerFormContext);
     },
     detach(context) {
-      $(context)
-        .find('#load-previous-threads-button')
-        .unbind('click', loadOldThreadWatcherHandler);
-      $(context)
-        .find('.private-message-inbox-thread-link')
-        .unbind('click', inboxThreadLinkListenerHandler);
+      Drupal.privateMessageInboxPrevious.detachEventListener(
+        context,
+        privateMessageInboxBlock.loadOldThreads.bind(privateMessageInboxBlock),
+      );
     },
   };
-})(jQuery, Drupal, drupalSettings, window, once);
+
+  /**
+   * Custom AJAX commands for private message inbox.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   The Drupal Ajax object.
+   * @param {object} response
+   *   Object holding the server response.
+   */
+  Drupal.AjaxCommands.prototype.insertInboxOldPrivateMessageThreads = (
+    ajax,
+    response,
+  ) => {
+    if (response.threads) {
+      privateMessageInboxBlock.insertPreviousThreads(response.threads);
+    }
+
+    if (!response.threads || !response.hasNext) {
+      Drupal.privateMessageInboxPrevious.slideDownButton();
+    }
+  };
+
+  /**
+   * Custom AJAX command to update the inbox with new threads.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   The Drupal Ajax object.
+   * @param {object} response
+   *   Object holding the server response.
+   */
+  Drupal.AjaxCommands.prototype.privateMessageInboxUpdate = (
+    ajax,
+    response,
+  ) => {
+    privateMessageInboxBlock.reorderInbox(
+      response.threadIds,
+      response.newThreads,
+    );
+  };
+
+  /**
+   * Custom AJAX command to trigger an inbox update.
+   */
+  Drupal.AjaxCommands.prototype.privateMessageTriggerInboxUpdate = () => {
+    privateMessageInboxBlock.updateInbox();
+  };
+})(Drupal, drupalSettings, window, once);
diff --git a/js/private_message_inbox_previous.js b/js/private_message_inbox_previous.js
new file mode 100644
index 0000000000000000000000000000000000000000..d470b7855d9add499d40bb1f943407b78cb57659
--- /dev/null
+++ b/js/private_message_inbox_previous.js
@@ -0,0 +1,61 @@
+/**
+ * @file
+ * Adds JavaScript functionality to the inbox previous button.
+ */
+
+((Drupal) => {
+  /**
+   * Previous functionality.
+   */
+  Drupal.privateMessageInboxPrevious = {
+    wrapperId: 'load-previous-threads-button-wrapper',
+    buttonId: 'load-previous-threads-button',
+
+    /**
+     * Display the previous button.
+     *
+     * @param {HTMLElement} blockElement
+     *   The element where the button will be added.
+     * @param {Function} callback
+     *   The function to call on button click.
+     */
+    displayButton(blockElement, callback) {
+      blockElement.insertAdjacentHTML(
+        'afterend',
+        `<div id="${this.wrapperId}">
+          <a href="#" id="${this.buttonId}" aria-label="${Drupal.t(
+            'Load previous threads',
+          )}">${Drupal.t('Load Previous')}</a>
+        </div>`,
+      );
+
+      const loadPreviousButton = document.getElementById(this.buttonId);
+      loadPreviousButton.addEventListener('click', callback);
+    },
+
+    /**
+     * Slides done the button.
+     */
+    slideDownButton() {
+      const buttonWrapper = document.getElementById(this.wrapperId);
+      if (buttonWrapper) {
+        Drupal.PrivateMessageSlide.toggleSlide(buttonWrapper, false);
+      }
+    },
+
+    /*
+     * Detaches event listener.
+     *
+     * @param {Document|HTMLElement} context
+     *   The context to search for the button.
+     * @param {Function} callback
+     *   The function to detach from the click event.
+     */
+    detachEventListener(context, callback) {
+      const button = context.querySelector(`#${this.buttonId}`);
+      if (button) {
+        button.removeEventListener('click', callback);
+      }
+    },
+  };
+})(Drupal);
diff --git a/js/private_message_notification_block.js b/js/private_message_notification_block.js
index 9cd4aeb9b48fec5eda1d2d7ea17e7749dd2f72fe..2a0ce56eadb8969729841a1e31986eb036db66b0 100644
--- a/js/private_message_notification_block.js
+++ b/js/private_message_notification_block.js
@@ -4,8 +4,8 @@
  */
 
 ((Drupal, drupalSettings, window) => {
-  let refreshRate;
   let checkingCountInProgress = false;
+  let updateTimeoutId = null;
 
   /**
    * Private message notification block.
@@ -87,6 +87,24 @@
         pageTitle.textContent = pageTitle.textContent.replace(titlePattern, '');
       }
     },
+
+    /**
+     * Sets a timeout for count updates.
+     */
+    scheduleCountUpdate() {
+      if (updateTimeoutId) {
+        window.clearTimeout(updateTimeoutId);
+      }
+
+      const refreshRate =
+        drupalSettings.privateMessageNotificationBlock.ajaxRefreshRate * 1000;
+      if (refreshRate) {
+        updateTimeoutId = window.setTimeout(
+          Drupal.privateMessageNotificationBlock.triggerCountCallback,
+          refreshRate,
+        );
+      }
+    },
   };
 
   /**
@@ -102,14 +120,8 @@
         context,
       ).shift();
 
-      refreshRate =
-        drupalSettings.privateMessageNotificationBlock.ajaxRefreshRate * 1000;
-
-      if (notificationWrapper && refreshRate) {
-        window.setTimeout(
-          Drupal.privateMessageNotificationBlock.triggerCountCallback,
-          refreshRate,
-        );
+      if (notificationWrapper) {
+        Drupal.privateMessageNotificationBlock.scheduleCountUpdate();
       }
     },
   };
@@ -131,11 +143,6 @@
     );
 
     checkingCountInProgress = false;
-    if (refreshRate) {
-      window.setTimeout(
-        Drupal.privateMessageNotificationBlock.triggerCountCallback,
-        refreshRate,
-      );
-    }
+    Drupal.privateMessageNotificationBlock.scheduleCountUpdate();
   };
 })(Drupal, drupalSettings, window);
diff --git a/js/private_message_slide.js b/js/private_message_slide.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcd6307601af07ff7c07a21e5ce65cf082b614d4
--- /dev/null
+++ b/js/private_message_slide.js
@@ -0,0 +1,45 @@
+/**
+ * @file
+ *
+ * Slide functions for Private Message module.
+ */
+
+((Drupal) => {
+  Drupal.PrivateMessageSlide = Drupal.PrivateMessageSlide || {};
+
+  /**
+   * Toggles slide-up and slide-down effects.
+   *
+   * @param {HTMLElement} element
+   *   The element to toggle.
+   * @param {boolean} expand
+   *   True to slide down, false to slide up.
+   */
+  Drupal.PrivateMessageSlide.toggleSlide = (element, expand) => {
+    // Ensure the element is valid
+    if (!element) {
+      return;
+    }
+
+    // Add the base slide class if not already present
+    element.classList.add('private-message-slide');
+
+    if (expand) {
+      element.style.height = '0';
+      element.classList.remove('private-message-slide__collapsed');
+
+      requestAnimationFrame(() => {
+        element.classList.add('private-message-slide__expanded');
+        element.style.height = `${element.scrollHeight}px`;
+      });
+    } else {
+      element.style.height = `${element.scrollHeight}px`;
+      element.classList.remove('private-message-slide__expanded');
+
+      requestAnimationFrame(() => {
+        element.classList.add('private-message-slide__collapsed');
+        element.style.height = '0';
+      });
+    }
+  };
+})(Drupal);
diff --git a/js/private_message_thread.js b/js/private_message_thread.js
index 288af094bc50abc43b036f82f02be893f4380b38..95922af68bc7a1ced0e8b56ac3a20c8cb2654816 100644
--- a/js/private_message_thread.js
+++ b/js/private_message_thread.js
@@ -338,8 +338,31 @@ Drupal.PrivateMessages.threadChange = {};
     }
   }
 
+  /**
+   * Click Handler executed when private message threads are clicked.
+   *
+   * Loads the thread into the private message window.
+   * @param {Event} e The event.
+   */
+  function inboxThreadLinkListenerHandler(e) {
+    e.preventDefault();
+    const { threadId } = e.currentTarget.dataset;
+    if (threadId) {
+      Drupal.PrivateMessages.loadThread(threadId);
+      Drupal.PrivateMessageUtils.setActiveThread(threadId);
+    }
+  }
+
   Drupal.behaviors.privateMessageThread = {
     attach(context) {
+      once(
+        'inbox-thread-link-listener',
+        '.private-message-inbox-thread-link',
+        context,
+      ).forEach((el) => {
+        el.addEventListener('click', inboxThreadLinkListenerHandler);
+      });
+
       init();
       loadPreviousListener(context);
       currentThreadId = threadWrapper
@@ -414,6 +437,14 @@ Drupal.PrivateMessages.threadChange = {};
       $(context)
         .find('#load-previous-messages')
         .unbind('click', loadPreviousListenerHandler);
+
+      // Unbind the 'click' event from '.private-message-inbox-thread-link' elements
+      const inboxThreadLinks = context.querySelectorAll(
+        '.private-message-inbox-thread-link',
+      );
+      inboxThreadLinks.forEach((link) => {
+        link.removeEventListener('click', inboxThreadLinkListenerHandler);
+      });
     },
   };
 
diff --git a/js/private_message_utils.js b/js/private_message_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ecdf637e95445bb8312df48838f1a84ce9c2c01
--- /dev/null
+++ b/js/private_message_utils.js
@@ -0,0 +1,46 @@
+/**
+ * @file
+ * Shared utility functions for Private Message module.
+ */
+
+((Drupal) => {
+  Drupal.PrivateMessageUtils = {
+    /**
+     * Creates a temporary DOM container.
+     *
+     * @param {string} html
+     *   The HTML string to parse.
+     * @return {NodeListOf<ChildNode>}
+     *   Parsed HTML content as NodeList.
+     */
+    parseHTML(html) {
+      const tempDiv = document.createElement('div');
+      tempDiv.innerHTML = html;
+
+      return document.createDocumentFragment().appendChild(tempDiv).childNodes;
+    },
+
+    /**
+     * Sets the active thread visually.
+     *
+     * @param {string} threadId
+     *   The thread ID.
+     */
+    setActiveThread(threadId) {
+      const activeThread = document.querySelector(
+        '.private-message-thread--full-container .active-thread',
+      );
+      if (activeThread) {
+        activeThread.classList.remove('active-thread');
+      }
+
+      const targetThread = document.querySelector(
+        `.private-message-thread--full-container .private-message-thread[data-thread-id="${threadId}"]`,
+      );
+      if (targetThread) {
+        targetThread.classList.remove('unread-thread');
+        targetThread.classList.add('active-thread');
+      }
+    },
+  };
+})(Drupal);
diff --git a/private_message.libraries.yml b/private_message.libraries.yml
index fdc3ed30d0ab5a69aa23f203797d1db4f4b20843..77a12bff50db3ff2ef6d2b41b10d689690b98c52 100644
--- a/private_message.libraries.yml
+++ b/private_message.libraries.yml
@@ -2,14 +2,31 @@ history_api:
   js:
     js/history_api.js: {}
 
+utils:
+  js:
+    js/private_message_utils.js: {}
+  dependencies:
+    - core/drupal
+
+slide:
+  js:
+    js/private_message_slide.js: { }
+  css:
+    theme:
+      css/private_message_slide.css: { }
+  dependencies:
+    - core/drupal
+
 inbox_block_script:
   js:
+    js/private_message_inbox_previous.js: {}
     js/private_message_inbox_block.js: {}
   dependencies:
-    - core/jquery
     - core/once
     - core/drupal.ajax
     - core/drupalSettings
+    - private_message/slide
+    - private_message/utils
 
 inbox_block_style:
   css:
@@ -37,6 +54,7 @@ private_message_thread_script:
     - core/drupalSettings
     - core/once
     - private_message/history_api
+    - private_message/utils
 
 private_message_thread_style:
   css:
diff --git a/tests/src/FunctionalJavascript/InboxBlockTest.php b/tests/src/FunctionalJavascript/InboxBlockTest.php
index 4cec4f2131c81bf94a581818a0c71cd4567f66a2..5f68a3d9c9e02e8ed7df9066d2de4bff31e666b5 100644
--- a/tests/src/FunctionalJavascript/InboxBlockTest.php
+++ b/tests/src/FunctionalJavascript/InboxBlockTest.php
@@ -41,7 +41,7 @@ class InboxBlockTest extends WebDriverTestBase {
   public function setUp(): void {
     parent::setUp();
     $this->attachFullNameField();
-    $this->createTestingUsers(3);
+    $this->createTestingUsers(4);
 
     $this->threads[] = $this->createThreadWithMessages([
       $this->users['a'],
@@ -85,6 +85,12 @@ class InboxBlockTest extends WebDriverTestBase {
    * Tests load previous functionality.
    */
   public function testLoadPrevious(): void {
+    // 2 more threads, together we have 4.
+    $this->createThreadWithMessages([
+      $this->users['a'],
+      $this->users['d'],
+    ], $this->users['d']);
+
     $this->createThreadWithMessages([
       $this->users['a'],
       $this->users['b'],
@@ -93,7 +99,7 @@ class InboxBlockTest extends WebDriverTestBase {
 
     $settings = [
       'thread_count' => 1,
-      'ajax_load_count' => 1,
+      'ajax_load_count' => 2,
       'ajax_refresh_rate' => 100,
     ];
     $this->drupalPlaceBlock('private_message_inbox_block', $settings);
@@ -105,13 +111,13 @@ class InboxBlockTest extends WebDriverTestBase {
       ->getPage()
       ->clickLink('Load Previous');
     $this->assertSession()->assertWaitOnAjaxRequest();
-    $this->assertEquals(2, $this->countThreads());
+    $this->assertEquals(3, $this->countThreads());
 
     $this->getSession()
       ->getPage()
       ->clickLink('Load Previous');
     $this->assertSession()->assertWaitOnAjaxRequest();
-    $this->assertEquals(3, $this->countThreads());
+    $this->assertEquals(4, $this->countThreads());
     $this->assertSession()
       ->pageTextNotContains('Load Previous');
   }
@@ -127,7 +133,12 @@ class InboxBlockTest extends WebDriverTestBase {
     $this->drupalPlaceBlock('private_message_inbox_block', $settings);
     $this->drupalLogin($this->users['a']);
 
-    $lastThread = $this->createThreadWithMessages([
+    $this->threads[] = $this->createThreadWithMessages([
+      $this->users['a'],
+      $this->users['d'],
+    ], $this->users['d']);
+
+    $this->threads[] = $this->createThreadWithMessages([
       $this->users['a'],
       $this->users['b'],
       $this->users['c'],
@@ -135,9 +146,23 @@ class InboxBlockTest extends WebDriverTestBase {
 
     $this->assertSession()->assertWaitOnAjaxRequest();
 
-    $this->assertSession()
-      ->elementExists('css', '.private-message-thread-inbox[data-thread-id="' . $lastThread->id() . '"]');
-    $this->assertEquals(3, $this->countThreads());
+    $this->assertEquals(4, $this->countThreads());
+
+    $threadElements = $this->getSession()
+      ->getPage()
+      ->findAll('css', '.private-message-thread-inbox');
+    $threadIds = [];
+    foreach ($threadElements as $threadElement) {
+      $threadIds[] = $threadElement->getAttribute('data-thread-id');
+    }
+
+    $expectedOrder = [];
+    foreach ($this->threads as $thread) {
+      $expectedOrder[] = $thread->id();
+    }
+    $expectedOrder = array_reverse($expectedOrder);
+
+    $this->assertEquals($expectedOrder, $threadIds, 'Threads are not in the expected order.');
   }
 
 }