diff --git a/reviewboard/static/rb/js/reviewRequestPage/views/reviewEntryView.es6.js b/reviewboard/static/rb/js/reviewRequestPage/views/reviewEntryView.es6.js
index 5206cc8483b1c792011c4afbaa79e2609ab53eac..d7dbdfe14308e558d0410742272060bed15b4651 100644
--- a/reviewboard/static/rb/js/reviewRequestPage/views/reviewEntryView.es6.js
+++ b/reviewboard/static/rb/js/reviewRequestPage/views/reviewEntryView.es6.js
@@ -17,10 +17,19 @@ RB.ReviewRequestPage.ReviewEntryView = ParentView.extend({
 
     /**
      * Initialize the view.
+     *
+     * Args:
+     *     options (object):
+     *         Options for the view.
+     *
+     * Option Args:
+     *     reviewRequestEditorView (RB.ReviewRequestEditorView):
+     *         The review request editor view.
      */
-    initialize() {
-        ParentView.prototype.initialize.call(this);
+    initialize(options) {
+        ParentView.prototype.initialize.call(this, options);
 
+        this.reviewRequestEditorView = options.reviewRequestEditorView;
         this._reviewView = null;
         this._draftBannerShown = false;
         this._$boxStatus = null;
@@ -66,6 +75,7 @@ RB.ReviewRequestPage.ReviewEntryView = ParentView.extend({
             $bannerFloatContainer: this._$box,
             $bannerParent: this.$('.banners'),
             bannerNoFloatContainerClass: 'collapsed',
+            reviewRequestEditorView: this.reviewRequestEditorView,
         });
 
         this._$boxStatus = this.$('.box-status');
diff --git a/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyEditorView.es6.js b/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyEditorView.es6.js
index 01ea5ef4f9af01521b8e5c09076de1624a26f7ce..b5b76197959fd6d510a6776e32c4866c60eb4187 100644
--- a/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyEditorView.es6.js
+++ b/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyEditorView.es6.js
@@ -125,6 +125,45 @@ RB.ReviewRequestPage.ReviewReplyEditorView = Backbone.View.extend({
         this._inlineEditorView.startEdit();
     },
 
+    /**
+     * Return whether this editor needs to be saved.
+     *
+     * Returns:
+     *     boolean:
+     *     Whether the comment editor has unsaved content.
+     */
+    needsSave() {
+        return this._inlineEditorView && this._inlineEditorView.isDirty();
+    },
+
+    /**
+     * Save the editor.
+     *
+     * Returns:
+     *     Promise:
+     *     A promise which resolves when the save is complete.
+     */
+    async save() {
+        const value = this._inlineEditorView.submit({
+            preventEvents: true,
+        });
+
+        if (value) {
+            const reviewRequestEditor = this.options.reviewRequestEditor;
+
+            if (reviewRequestEditor) {
+                reviewRequestEditor.decr('editCount');
+            }
+
+            this.model.set({
+                richText: this._inlineEditorView.textEditor.richText,
+                text: value,
+            });
+
+            await this.model.save();
+        }
+    },
+
     /**
      * Create a comment editor for an element.
      *
diff --git a/reviewboard/static/rb/js/reviewRequestPage/views/reviewView.es6.js b/reviewboard/static/rb/js/reviewRequestPage/views/reviewView.es6.js
index 2f8bf8f7952f9c87eef8097bfb386555c5667fe7..53624c113e0dc4d16f474d1ccbaccf0e1067ae2a 100644
--- a/reviewboard/static/rb/js/reviewRequestPage/views/reviewView.es6.js
+++ b/reviewboard/static/rb/js/reviewRequestPage/views/reviewView.es6.js
@@ -6,10 +6,22 @@
 RB.ReviewRequestPage.ReviewView = Backbone.View.extend({
     /**
      * Initialize the view.
+     *
+     * Args:
+     *     options (object):
+     *         Options for the view.
+     *
+     * Option Args:
+     *     entryModel (RB.ReviewRequestPage.Entry):
+     *         The entry model.
+     *
+     *     reviewRequestEditorView (RB.ReviewRequestEditorView):
+     *         The review request editor view.
      */
     initialize(options) {
         this.options = options;
         this.entryModel = options.entryModel;
+        this.reviewRequestEditorView = options.reviewRequestEditorView;
 
         this._bannerView = null;
         this._draftBannerShown = false;
@@ -63,7 +75,8 @@ RB.ReviewRequestPage.ReviewView = Backbone.View.extend({
                  * We make this conditional to make unit tests easier to write.
                  */
                 if (banner) {
-                    banner.model.updateReplyDraftState(this._reviewReply, hasDraft);
+                    banner.model.updateReplyDraftState(
+                        this._reviewReply, hasDraft);
                 }
             }
 
@@ -151,6 +164,10 @@ RB.ReviewRequestPage.ReviewView = Backbone.View.extend({
             this._replyEditors.push(editor);
             this._replyEditorViews.push(view);
 
+            if (this.reviewRequestEditorView) {
+                this.reviewRequestEditorView.addReviewReplyEditorView(view);
+            }
+
             if (editor.get('hasDraft')) {
                 this._replyDraftsCount++;
             }
diff --git a/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts b/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
index 7ab693e88cd2015d73cb6e3ceb7edec01cc727ea..0e6ecc4541f32145734b9e5e61aab5357856e945 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts
@@ -470,6 +470,9 @@ export class ReviewRequestEditorView extends BaseView<ReviewRequestEditor> {
     /** The views for all of the file attachment thumbnails. */
     #fileAttachmentThumbnailViews: RB.FileAttachmentThumbnailView[] = [];
 
+    /** The views for all of the review reply editors. */
+    #reviewReplyEditorViews: RB.ReviewReplyEditorView[] = [];
+
     /**
      * The active banner, if available.
      *
@@ -713,6 +716,23 @@ export class ReviewRequestEditorView extends BaseView<ReviewRequestEditor> {
         this.banner.render();
     }
 
+    /**
+     * Add a review reply editor view.
+     *
+     * These views are constructed by the individual review views. We keep
+     * track of them here so that we can save any open editors when performing
+     * publish operations.
+     *
+     * Args:
+     *     reviewReplyEditorView (RB.ReviewRequestPage.ReviewReplyEditorView):
+     *          The review reply editor view.
+     */
+    addReviewReplyEditorView(
+        reviewReplyEditorView: RB.ReviewRequestPage.ReviewReplyEditorView,
+    ) {
+        this.#reviewReplyEditorViews.push(reviewReplyEditorView);
+    }
+
     /**
      * Handle a click on the "Publish Draft" button.
      *
@@ -740,6 +760,11 @@ export class ReviewRequestEditorView extends BaseView<ReviewRequestEditor> {
             Object.values(this.#fieldViews)
                 .filter(field => field.needsSave())
                 .map(field => field.finishSave()));
+
+        await Promise.all(
+            this.#reviewReplyEditorViews
+                .filter(view => view.needsSave())
+                .map(field => field.save()));
     }
 
     /**
diff --git a/reviewboard/static/rb/js/reviews/views/reviewablePageView.ts b/reviewboard/static/rb/js/reviews/views/reviewablePageView.ts
index 0f70ab6331b351a3667a5344092faf8e9f634a66..68de47ba763dd56468c09f0b4b5a1fdc4aec906c 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewablePageView.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewablePageView.ts
@@ -186,9 +186,9 @@ export class ReviewablePageView<
         ReviewablePageViewOptions
 > extends PageView<TModel, TElement, TExtraViewOptions> {
     static events: EventsHash = {
-        'click #action-edit-review': '_onEditReviewClicked',
+        'click #action-legacy-edit-review': '_onEditReviewClicked',
         'click #action-legacy-add-general-comment': 'addGeneralComment',
-        'click #action-ship-it': 'shipIt',
+        'click #action-legacy-ship-it': 'shipIt',
         'click .rb-o-mobile-menu-label': '_onMenuClicked',
     };
 
diff --git a/reviewboard/static/rb/js/reviews/views/tests/reviewablePageViewTests.ts b/reviewboard/static/rb/js/reviews/views/tests/reviewablePageViewTests.ts
index fb0f9b0121fad254a1ed996e7a490800f8e79f79..4a633ac59a5505a081935f2c58c59d09dcacdca7 100644
--- a/reviewboard/static/rb/js/reviews/views/tests/reviewablePageViewTests.ts
+++ b/reviewboard/static/rb/js/reviews/views/tests/reviewablePageViewTests.ts
@@ -27,12 +27,11 @@ suite('rb/pages/views/ReviewablePageView', function() {
         <div id="unified-banner">
          <div class="rb-c-unified-banner__mode-selector"></div>
         </div>
-        <a href="#" id="action-edit-review">Edit Review</a>
-        <a href="#" id="action-ship-it">Ship It</a>
+        <a href="#" id="action-legacy-edit-review">Edit Review</a>
+        <a href="#" id="action-legacy-ship-it">Ship It</a>
     `;
 
     let $editReview;
-    let $shipIt;
     let page;
     let pageView;
 
@@ -43,8 +42,7 @@ suite('rb/pages/views/ReviewablePageView', function() {
 
         RB.DnDUploader.instance = null;
 
-        $editReview = $container.find('#action-edit-review');
-        $shipIt = $container.find('#action-ship-it');
+        $editReview = $container.find('#action-legacy-edit-review');
 
         page = new ReviewablePage({
             checkForUpdates: false,
