diff --git a/reviewboard/static/rb/js/reviews/index.ts b/reviewboard/static/rb/js/reviews/index.ts
index 5e4873a48485d16fcc4e0b05b7111841567e2f8f..00d3bfad24d9ac15a1287996bce17ca0d0e77f57 100644
--- a/reviewboard/static/rb/js/reviews/index.ts
+++ b/reviewboard/static/rb/js/reviews/index.ts
@@ -54,6 +54,7 @@ export {
 } from './views/fileAttachmentThumbnailView';
 export { ImageReviewableView } from './views/imageReviewableView';
 export { MarkdownReviewableView } from './views/markdownReviewableView';
+export { PublishButtonView } from './views/publishButton';
 export { RegionCommentBlockView } from './views/regionCommentBlockView';
 export { ReviewDialogView } from './views/reviewDialogView';
 export { ReviewRequestEditorView } from './views/reviewRequestEditorView';
diff --git a/reviewboard/static/rb/js/reviews/views/publishButton.ts b/reviewboard/static/rb/js/reviews/views/publishButton.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15c94e050b39b7a35cb504e8886b922d2b2abf1a
--- /dev/null
+++ b/reviewboard/static/rb/js/reviews/views/publishButton.ts
@@ -0,0 +1,125 @@
+/**
+ * The publish button for reviews.
+ *
+ * Version Added:
+ *      TODO
+ */
+
+import {
+    MenuButtonView,
+    MenuItem,
+    MenuItemType,
+    MenuItemsCollection,
+} from '@beanbag/ink';
+import {
+    type EventsHash,
+    BaseModel,
+    spina,
+} from '@beanbag/spina';
+import _ from 'underscore';
+
+
+import {
+    UserSession,
+} from 'reviewboard/common';
+import { ReviewRequestEditor } from '../models/reviewRequestEditorModel';
+
+/**
+ * The publish button.
+ *
+ * This is the standard publish button that provides the standard for
+ * publishing buttons across the website. It includes a dropdown checkbox list
+ * that allows the user to select the settings they prefer (emailing and
+ * archiving) when publishing. It is modular by nature, allowing for
+ * new options for the checkbox list in the future.
+ *
+ * Version Added:
+ *      TODO
+ */
+@spina
+export class PublishButtonView<TModel extends BaseModel>
+extends MenuButtonView<TModel> {
+    static modelEvents: EventsHash = {
+        'change:draftModes change:selectedDraftMode': '_update',
+    };
+
+    /**
+     * Initialize the publish button.
+     */
+    initialize() {
+        const reviewRequestEditor = this._getReviewRequestEditor();
+        const menuItems = new MenuItemsCollection();
+        let sendEmailItem: MenuItem | null = null;
+
+        if (reviewRequestEditor.get('showSendEmail')) {
+            sendEmailItem = new MenuItem({
+                checked: true,
+                label: _`Send E-Mail`,
+                type: MenuItemType.CHECKBOX_ITEM,
+            });
+            menuItems.add(sendEmailItem);
+        }
+
+        const publishAndArchiveItem: MenuItem = new MenuItem({
+            checked: UserSession.instance.get('publishAndArchive'),
+            label: _`Archive After Publishing`,
+            type: MenuItemType.CHECKBOX_ITEM,
+        });
+        menuItems.add(publishAndArchiveItem);
+
+        super.initialize({
+            dropdownButtonAriaLabel: _`Open publish options`,
+            hasActionButton: true,
+            label: _`Publish All`,
+            menuAriaLabel: _`Publish options`,
+            menuIconName: 'fa fa-gear',
+            menuItems: menuItems,
+            onActionButtonClick: () => {
+                this._action(publishAndArchiveItem, sendEmailItem);
+            },
+        });
+    }
+
+    /**
+     * TODO Doc Comment
+     */
+    protected onComponentInitialRender() {
+        super.onComponentInitialRender();
+        this._update();
+    }
+
+    /**
+     * Update the state of the publish button.
+     */
+    protected _update() {
+        // Does nothing for now. Depends on Model used in subclasses.
+    }
+
+    /**
+     * Return the review request editor.
+     *
+     * Returns:
+     *      BaseModel
+     *      Used to access the showSendEmail setting.
+     */
+    protected _getReviewRequestEditor(): ReviewRequestEditor {
+        return this.model.get('reviewRequestEditor');
+    }
+
+    /**
+     * Trigger the publish operation.
+     *
+     * Args:
+     *      showSendEmailItem (MenuItem):
+     *          The checkbox object for emails.
+     */
+    protected _action(
+        publishAndArchiveItem: MenuItem,
+        sendEmailItem: MenuItem
+    ) {
+        this.trigger('publish', {
+            archive: publishAndArchiveItem.get('checked'),
+            trivial: sendEmailItem !== null && !sendEmailItem.get('checked'),
+        });
+    }
+}
diff --git a/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts b/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
index add9a4df6a20fda0d4bdb96dc20888f22963f667..4516accfdf24e017ef6fada2374b4950858bdfe4 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewDialogView.ts
@@ -4,12 +4,16 @@
 
 import {
     type MenuButtonView,
-    MenuItemsCollection,
+    MenuItem,
     craft,
     paint,
 } from '@beanbag/ink';
 import { BaseView, spina } from '@beanbag/spina';
 
+import {
+    PublishButtonView,
+} from './publishButton';
+
 import {
     type Review,
     ClientCommChannel,
@@ -900,8 +904,8 @@ class HeaderFooterCommentView extends BaseView<
      */
     _getRawValueFieldsName(): string {
         return UserSession.instance.get('defaultUseRichText')
-               ? 'markdownTextFields'
-               : 'rawTextFields';
+            ? 'markdownTextFields'
+            : 'rawTextFields';
     }
 }
 
@@ -1053,6 +1057,47 @@ interface ReviewDialogViewCreationOptions extends ReviewDialogViewOptions {
 }
 
 
+/**
+ * The publish button.
+ *
+ * Version Added:
+ *     TODO
+ */
+@spina
+class ReviewDialogPublishButtonView extends
+    PublishButtonView<ReviewRequestEditor> {
+    /**
+     * Returns the review request editor.
+     *
+     * Returns:
+     *      ReviewRequestEditor:
+     *      Used to access the sendEmail and publishAndArchive
+     *      settings.
+     */
+    protected _getReviewRequestEditor(): ReviewRequestEditor {
+        return this.model;
+    }
+
+    /**
+     * Calls the '_saveReview' function as it's action.
+     *
+     * Args:
+     *      showSendEmailItem (MenuItem):
+     *          The checkbox object for emails.
+     */
+    protected _action(
+        publishAndArchiveItem: MenuItem,
+        showSendEmailItem: MenuItem
+    ) {
+        this.trigger('_saveReview', true, {
+            archive: publishAndArchiveItem.get('checked'),
+            trivial: (showSendEmailItem === null ||
+                      !showSendEmailItem.get('checked')),
+        });
+    }
+}
+
+
 /**
  * Creates a dialog for modifying a draft review.
  *
@@ -1118,6 +1163,7 @@ export class ReviewDialogView extends BaseView<
             model: options.review,
             reviewRequestEditor: options.reviewRequestEditor,
         });
+
         this.instance = dialog;
 
         dialog.render();
@@ -1484,7 +1530,7 @@ export class ReviewDialogView extends BaseView<
             this._handleEmptyReview();
 
             this.trigger('loadCommentsDone');
-        } catch(err) {
+        } catch (err) {
             alert(err.message); // TODO: provide better output.
         }
     }
@@ -1554,34 +1600,11 @@ export class ReviewDialogView extends BaseView<
             </div>
         `;
 
-        const menuItems = new MenuItemsCollection([
-            {
-                label: _`... and only e-mail the owner`,
-                onClick: () => {
-                    this._saveReview(true, {
-                        publishToOwnerOnly: true,
-                    });
-                },
-            },
-            {
-                label: _`... and archive the review request`,
-                onClick: () => {
-                    this._saveReview(true, {
-                        publishAndArchive: true,
-                    });
-                },
-            },
-        ]);
-
-        const publishButton = craft<MenuButtonView>`
-            <Ink.MenuButton
-              hasActionButton
-              label="${_`Publish Review`}"
-              menuAriaLabel="${_`More publishing options`}"
-              menuItems=${menuItems}
-              onActionButtonClick=${() => this._saveReview(true)}
-              type="primary"/>
-        `;
+        const publishButton = new ReviewDialogPublishButtonView({
+            model: this.options.reviewRequestEditor,
+        });
+        this.listenTo(publishButton, '_saveReview', this._saveReview);
+        publishButton.render();
 
         const rightButtonsEl = craft<HTMLElement>`
             <div class="review-dialog-buttons-right">
@@ -1713,15 +1736,15 @@ export class ReviewDialogView extends BaseView<
     async _saveReview(
         publish: boolean,
         options: {
-            publishAndArchive?: boolean;
-            publishToOwnerOnly?: boolean;
+            archive?: boolean;
+            trivial?: boolean;
         } = {},
     ): Promise<void> {
-        if (publish && options.publishToOwnerOnly) {
+        if (publish && options.trivial) {
             this.model.set('publishToOwnerOnly', true);
         }
 
-        if (publish && options.publishAndArchive) {
+        if (publish && options.archive) {
             this.model.set('publishAndArchive', true);
         }
 
@@ -1800,7 +1823,7 @@ export class ReviewDialogView extends BaseView<
         model.set({
             forceTextType: 'html',
             includeTextTypes: this.#defaultUseRichText
-                              ? 'raw,markdown' : 'raw',
+                ? 'raw,markdown' : 'raw',
         });
     }
 
diff --git a/reviewboard/static/rb/js/reviews/views/tests/index.ts b/reviewboard/static/rb/js/reviews/views/tests/index.ts
index 4121b2c85601193450b9a1edfaf6fee2b1cbb986..13a2ca3fcc423fae3560ed6e9965531e69cf4d5c 100644
--- a/reviewboard/static/rb/js/reviews/views/tests/index.ts
+++ b/reviewboard/static/rb/js/reviews/views/tests/index.ts
@@ -3,6 +3,7 @@ import './commentIssueBarViewTests';
 import './diffReviewableViewTests';
 import './diffViewerPageViewTests';
 import './fileAttachmentThumbnailViewTests';
+import './publishButtonViewTests';
 import './reviewDialogViewTests';
 import './reviewRequestEditorViewTests';
 import './reviewRequestFieldViewsTests';
diff --git a/reviewboard/static/rb/js/reviews/views/tests/publishButtonViewTests.ts b/reviewboard/static/rb/js/reviews/views/tests/publishButtonViewTests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3746731b2ea3bfcd76e9dd83aa1a57a99f83ab22
--- /dev/null
+++ b/reviewboard/static/rb/js/reviews/views/tests/publishButtonViewTests.ts
@@ -0,0 +1,251 @@
+import { suite } from '@beanbag/jasmine-suites';
+import {
+    afterEach,
+    beforeEach,
+    describe,
+    expect,
+    it,
+    spyOn,
+    jasmine,
+} from 'jasmine-core';
+
+import {
+    ReviewRequest,
+    UserSession,
+} from 'reviewboard/common';
+import {
+    PublishButtonView,
+    ReviewRequestEditor,
+    UnifiedBanner,
+} from 'reviewboard/reviews';
+
+suite('rb/views/PublishButtonView', function () {
+    let model: UnifiedBanner;
+    let publishButtonView: PublishButtonView | null;
+
+    afterEach(function () {
+        publishButtonView.remove();
+    });
+
+    describe('initialization with both options enabled', function () {
+        beforeEach(function () {
+            UserSession.instance.set('publishAndArchive', true);
+
+            const reviewRequest = new ReviewRequest({
+                id: 123,
+                'public': true,
+                state: ReviewRequest.PENDING,
+            });
+            const reviewRequestEditor = new ReviewRequestEditor({
+                commentIssueManager: new RB.CommentIssueManager(),
+                mutableByUser: true,
+                reviewRequest: reviewRequest,
+                showSendEmail: true,
+                statusMutableByUser: true,
+            });
+            const pendingReview = reviewRequest.createReview();
+            spyOn(reviewRequest, 'ready').and.resolveTo();
+            spyOn(reviewRequest.draft, 'ready').and.resolveTo();
+            spyOn(pendingReview, 'ready').and.resolveTo();
+
+            model = new UnifiedBanner({
+                pendingReview: pendingReview,
+                reviewRequest: reviewRequest,
+                reviewRequestEditor: reviewRequestEditor,
+            });
+
+            publishButtonView = new PublishButtonView({
+                model: model,
+            });
+
+            publishButtonView.render();
+        });
+
+        it('should initialize both email and archive as checked', function () {
+            const menuItems = publishButtonView.menuView.menuItems;
+            expect(menuItems.length).toBe(2);
+
+            const sendEmailItem = menuItems.at(0);
+            expect(sendEmailItem.get('label')).toBe('Send E-Mail');
+            expect(sendEmailItem.get('checked')).toBe(true);
+
+            const archiveItem = menuItems.at(1);
+            expect(archiveItem.get('label')).toBe('Archive After Publishing');
+            expect(archiveItem.get('checked')).toBe(true);
+        });
+
+        it('should initialize archive as not checked', function () {
+            UserSession.instance.set('publishAndArchive', false);
+
+            publishButtonView.remove();
+            publishButtonView = new PublishButtonView({
+                model: model,
+            });
+            publishButtonView.render();
+
+            const menuItems = publishButtonView.menuView.menuItems;
+            expect(menuItems.length).toBe(2);
+
+            const sendEmailItem = menuItems.at(0);
+            expect(sendEmailItem.get('label')).toBe('Send E-Mail');
+            expect(sendEmailItem.get('checked')).toBe(true);
+
+            const archiveItem = menuItems.at(1);
+            expect(archiveItem.get('label')).toBe('Archive After Publishing');
+            expect(archiveItem.get('checked')).toBe(false);
+        });
+    });
+
+    describe('initialization without emails enabled', function () {
+        beforeEach(function () {
+            UserSession.instance.set('publishAndArchive', true);
+
+            const reviewRequest = new ReviewRequest({
+                id: 123,
+                'public': true,
+                state: ReviewRequest.PENDING,
+            });
+            const reviewRequestEditor = new ReviewRequestEditor({
+                commentIssueManager: new RB.CommentIssueManager(),
+                mutableByUser: true,
+                reviewRequest: reviewRequest,
+                showSendEmail: false,
+                statusMutableByUser: true,
+            });
+            const pendingReview = reviewRequest.createReview();
+            spyOn(reviewRequest, 'ready').and.resolveTo();
+            spyOn(reviewRequest.draft, 'ready').and.resolveTo();
+            spyOn(pendingReview, 'ready').and.resolveTo();
+
+            model = new UnifiedBanner({
+                pendingReview: pendingReview,
+                reviewRequest: reviewRequest,
+                reviewRequestEditor: reviewRequestEditor,
+            });
+
+            publishButtonView = new PublishButtonView({
+                model: model,
+            });
+
+            publishButtonView.render();
+        });
+
+        it('should initialize only archive as checked', function () {
+            const menuItems = publishButtonView.menuView.menuItems;
+            expect(menuItems.length).toBe(1);
+
+            const archiveItem = menuItems.at(0);
+            expect(archiveItem.get('label')).toBe('Archive After Publishing');
+            expect(archiveItem.get('checked')).toBe(true);
+        });
+
+        it('should initialize only archive as not checked', function () {
+            UserSession.instance.set('publishAndArchive', false);
+
+            publishButtonView.remove();
+            publishButtonView = new PublishButtonView({
+                model: model,
+            });
+            publishButtonView.render();
+
+            const menuItems = publishButtonView.menuView.menuItems;
+            expect(menuItems.length).toBe(1);
+
+            const archiveItem = menuItems.at(0);
+            expect(archiveItem.get('label')).toBe('Archive After Publishing');
+            expect(archiveItem.get('checked')).toBe(false);
+        });
+    });
+
+    describe('Publish action', function () {
+        it('should trigger the publish event handler', function () {
+            UserSession.instance.set('publishAndArchive', true);
+
+            const reviewRequest = new ReviewRequest({
+                id: 123,
+                'public': true,
+                state: ReviewRequest.PENDING,
+            });
+            const reviewRequestEditor = new ReviewRequestEditor({
+                commentIssueManager: new RB.CommentIssueManager(),
+                mutableByUser: true,
+                reviewRequest: reviewRequest,
+                showSendEmail: true,
+                statusMutableByUser: true,
+            });
+            const pendingReview = reviewRequest.createReview();
+            spyOn(reviewRequest, 'ready').and.resolveTo();
+            spyOn(reviewRequest.draft, 'ready').and.resolveTo();
+            spyOn(pendingReview, 'ready').and.resolveTo();
+
+            model = new UnifiedBanner({
+                pendingReview: pendingReview,
+                reviewRequest: reviewRequest,
+                reviewRequestEditor: reviewRequestEditor,
+            });
+
+            publishButtonView = new PublishButtonView({
+                id: 'my-button',
+                model: model,
+            });
+
+            publishButtonView.render().$el.appendTo($testsScratch);
+
+            const onMyEvent = jasmine.createSpy('onClick');
+
+            publishButtonView.on('publish', onMyEvent);
+            publishButtonView.actionButtonView.$el.click();
+
+            expect(onMyEvent).toHaveBeenCalledWith({
+                archive: true,
+                trivial: false,
+            });
+        });
+
+        it('should trigger the publish event handler 2', function () {
+            UserSession.instance.set('publishAndArchive', false);
+
+            const reviewRequest = new ReviewRequest({
+                id: 123,
+                'public': true,
+                state: ReviewRequest.PENDING,
+            });
+            const reviewRequestEditor = new ReviewRequestEditor({
+                commentIssueManager: new RB.CommentIssueManager(),
+                mutableByUser: true,
+                reviewRequest: reviewRequest,
+                showSendEmail: true,
+                statusMutableByUser: true,
+            });
+            const pendingReview = reviewRequest.createReview();
+            spyOn(reviewRequest, 'ready').and.resolveTo();
+            spyOn(reviewRequest.draft, 'ready').and.resolveTo();
+            spyOn(pendingReview, 'ready').and.resolveTo();
+
+            model = new UnifiedBanner({
+                pendingReview: pendingReview,
+                reviewRequest: reviewRequest,
+                reviewRequestEditor: reviewRequestEditor,
+            });
+
+            publishButtonView = new PublishButtonView({
+                id: 'my-button',
+                model: model,
+            });
+            publishButtonView.render().$el.appendTo($testsScratch);
+
+            publishButtonView.menuView.menuItems.at(0).set('checked', false);
+
+
+            const onMyEvent = jasmine.createSpy('onClick');
+
+            publishButtonView.on('publish', onMyEvent);
+            publishButtonView.actionButtonView.$el.click();
+
+            expect(onMyEvent).toHaveBeenCalledWith({
+                archive: false,
+                trivial: true,
+            });
+        });
+    });
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts b/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
index 84763c11f07465b45c459eadd6e276343cf15253..6af4e40a6505d1e7118c58c6080f5c9b059ae1c6 100644
--- a/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
+++ b/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
@@ -2,10 +2,7 @@
  * The unified banner view.
  */
 import {
-    MenuButtonView,
     MenuItem,
-    MenuItemType,
-    MenuItemsCollection,
     MenuView,
     craft,
     paint,
@@ -33,6 +30,7 @@ import {
     UnifiedBanner,
 } from '../models/unifiedBannerModel';
 import { type ReviewRequestEditorView } from './reviewRequestEditorView';
+import { PublishButtonView } from './publishButton';
 import { ChangeDescriptionFieldView } from './reviewRequestFieldViews';
 
 
@@ -198,7 +196,7 @@ class DraftModeMenu extends BaseView<UnifiedBanner> {
                               aria-hidden="true"/>
                         ${text}
                     `,
-                    {empty: true});
+                    { empty: true });
             } else {
                 newMenuItems.push(new MenuItem({
                     label: text,
@@ -218,75 +216,14 @@ class DraftModeMenu extends BaseView<UnifiedBanner> {
  * The publish button.
  *
  * Version Added:
- *     6.0
+ *     TODO
  */
 @spina
-class PublishButtonView extends MenuButtonView<UnifiedBanner> {
-    static modelEvents = {
-        'change:draftModes change:selectedDraftMode': '_update',
-    };
-
-    /**********************
-     * Instance variables *
-     **********************/
-
-    #$archiveCheckbox: JQuery;
-    #$trivialCheckbox: JQuery;
-
-    /**
-     * Initialize the view.
-     */
-    initialize() {
-        const reviewRequestEditor = this.model.get('reviewRequestEditor');
-        const menuItems = new MenuItemsCollection();
-
-        let showSendEmailItem: MenuItem | null = null;
-
-        if (reviewRequestEditor.get('showSendEmail')) {
-            showSendEmailItem = new MenuItem({
-                checked: true,
-                label: _`Send E-Mail`,
-                type: MenuItemType.CHECKBOX_ITEM,
-            });
-            menuItems.add(showSendEmailItem);
-        }
-
-        const archiveItem = new MenuItem({
-            label: _`Archive after publishing`,
-            type: MenuItemType.CHECKBOX_ITEM,
-        });
-        menuItems.add(archiveItem);
-
-        super.initialize({
-            dropdownButtonAriaLabel: _`Open publish options`,
-            hasActionButton: true,
-            label: _`Publish All`,
-            menuAriaLabel: _`Publish options`,
-            menuIconName: 'fa fa-gear',
-            menuItems: menuItems,
-            onActionButtonClick: () => {
-                this.trigger('publish', {
-                    archive: archiveItem.get('checked'),
-                    trivial: showSendEmailItem === null ||
-                             !showSendEmailItem.get('checked'),
-                });
-            },
-        });
-    }
-
+class BannerPublishButtonView extends PublishButtonView<UnifiedBanner> {
     /**
-     * Handle the initial rendering of the menu button.
+     * Update the state of the publish button based on the UnifiedBanner model.
      */
-    protected onComponentInitialRender() {
-        super.onComponentInitialRender();
-
-        this._update()
-    }
-
-    /**
-     * Update the state of the publish button.
-     */
-    private _update() {
+    protected _update() {
         const model = this.model;
         const draftModes = model.get('draftModes');
         const selectedDraftMode = model.get('selectedDraftMode');
@@ -296,8 +233,8 @@ class PublishButtonView extends MenuButtonView<UnifiedBanner> {
         }
 
         this.label = (draftModes[selectedDraftMode].multiple
-                      ? _`Publish All`
-                      : _`Publish`);
+            ? _`Publish All`
+            : _`Publish`);
     }
 }
 
@@ -387,8 +324,8 @@ export class UnifiedBannerView extends FloatingBannerView<
     /** The draft mode menu. */
     #modeMenu: DraftModeMenu;
 
-    /** The publish button. */
-    #publishButton: PublishButtonView;
+    /** The publish button view. */
+    #publishButton: BannerPublishButtonView;
 
     /** The review request editor view. */
     #reviewRequestEditorView: ReviewRequestEditorView;
@@ -494,7 +431,7 @@ export class UnifiedBannerView extends FloatingBannerView<
         });
         this.#modeMenu.renderInto(this.#$modeSelector);
 
-        this.#publishButton = new PublishButtonView({
+        this.#publishButton = new BannerPublishButtonView({
             model: model,
         });
         this.#publishButton.$el.prependTo(this.#$draftActions);
@@ -811,7 +748,7 @@ export class UnifiedBannerView extends FloatingBannerView<
             } else {
                 console.error('Discard reached with no active drafts.');
             }
-        } catch(err) {
+        } catch (err) {
             alert(err.xhr.errorText);
         }
     }
