diff --git a/reviewboard/static/rb/js/common/resources/index.ts b/reviewboard/static/rb/js/common/resources/index.ts
index 41b6e26c2622bf0db5db0f18b1a197f8aa196fa0..352e788c0b7f2bf0ad5fdf34e86413f3266be9c1 100644
--- a/reviewboard/static/rb/js/common/resources/index.ts
+++ b/reviewboard/static/rb/js/common/resources/index.ts
@@ -1,4 +1,6 @@
 export { BaseResource } from './models/baseResourceModel';
+export { DraftResourceModelMixin } from './models/draftResourceModelMixin';
 export { ResourceCollection } from './collections/resourceCollection';
 export { Review } from './models/reviewModel';
+export { ReviewReply } from './models/reviewReplyModel';
 export * as JSONSerializers from './utils/serializers';
diff --git a/reviewboard/static/rb/js/common/resources/models/reviewModel.ts b/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
index 04f13c9a8b57bd63e4bb979e4cc3e60679e94004..f3b4668b395decc32af254e09c389e8c73132bf7 100644
--- a/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/reviewModel.ts
@@ -5,6 +5,7 @@
 import { spina } from '@beanbag/spina';
 
 import { BaseResource, BaseResourceAttrs } from './baseResourceModel';
+import { ReviewReply } from './reviewReplyModel';
 import * as JSONSerializers from '../utils/serializers';
 
 
@@ -31,7 +32,7 @@ interface ReviewAttrs extends BaseResourceAttrs {
     bodyTopRichText: boolean;
 
     /** The draft reply to this review, if any. */
-    draftReply: RB.ReviewReply;
+    draftReply: ReviewReply;
 
     /** The text format type to request for text fields in all responses. */
     forceTextType: string;
@@ -91,7 +92,9 @@ interface ReviewResourceData {
     body_bottom_text_type: string;
     body_top: string;
     body_top_text_type: string;
+    force_text_type: string;
     html_text_fields: { [key: string]: string };
+    include_text_types: string;
     markdown_text_fields: { [key: string]: string };
     public: boolean;
     raw_text_fields: { [key: string]: string };
@@ -141,7 +144,7 @@ interface CreateDiffCommentOptions {
  * A review.
  *
  * This corresponds to a top-level review. Replies are encapsulated in
- * RB.ReviewReply.
+ * ReviewReply.
  */
 @spina({
     prototypeAttrs: [
@@ -431,11 +434,11 @@ export class Review extends BaseResource<ReviewAttrs> {
      *     RB.ReviewReply:
      *     The new reply object.
      */
-    createReply(): RB.ReviewReply {
+    createReply(): ReviewReply {
         let draftReply = this.get('draftReply');
 
         if (draftReply === null) {
-            draftReply = new RB.ReviewReply({
+            draftReply = new ReviewReply({
                 parentObject: this,
             });
             this.set('draftReply', draftReply);
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/index.ts b/reviewboard/static/rb/js/common/resources/models/tests/index.ts
index 46bc9e5d957022906e475ed5cedc340e4e316b6f..5a47057696d7f9417169821f8c977677b95e76e2 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/index.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/index.ts
@@ -1,2 +1,3 @@
 import './baseResourceModelTests';
 import './reviewModelTests';
+import './reviewReplyModelTests';
diff --git a/reviewboard/static/rb/js/resources/models/draftResourceModelMixin.es6.js b/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
similarity index 85%
rename from reviewboard/static/rb/js/resources/models/draftResourceModelMixin.es6.js
rename to reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
index 574780e1bdd5440a73da52182282ca81376958c2..73c1194b451b9549b14752ce1508a9a87c07d655 100644
--- a/reviewboard/static/rb/js/resources/models/draftResourceModelMixin.es6.js
+++ b/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
@@ -1,3 +1,17 @@
+/**
+ * Mixin for resources that have special "draft" URLs.
+ */
+
+import { UserSession } from 'reviewboard/common/models/userSessionModel';
+
+
+interface ReadyOptions extends Backbone.PersistenceOptions {
+    ready?: ((modelOrCollection: unknown,
+              response: unknown,
+              options: unknown) => void) | undefined;
+}
+
+
 /**
  * Mixin for resources that have special "draft" URLs.
  *
@@ -8,7 +22,7 @@
  * These resources need a little more logic to look up the draft state and
  * craft the proper URL. They can use this mixin to do that work for them.
  */
-RB.DraftResourceModelMixin = {
+export const DraftResourceModelMixin = {
     /**
      * Call a function when the object is ready to use.
      *
@@ -32,7 +46,10 @@ RB.DraftResourceModelMixin = {
      *     Promise:
      *     A promise which resolves when the operation is complete.
      */
-    async ready(options={}, context=undefined) {
+    async ready(
+        options: ReadyOptions = {},
+        context: unknown = undefined,
+    ): Promise<void> {
         if (_.isFunction(options.success) ||
             _.isFunction(options.error) ||
             _.isFunction(options.complete) ||
@@ -40,6 +57,7 @@ RB.DraftResourceModelMixin = {
             console.warn('RB.DraftResourceModelMixin.ready was ' +
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
+
             return RB.promiseToCallbacks(
                 options, context, newOptions => this.ready(newOptions));
         }
@@ -82,13 +100,17 @@ RB.DraftResourceModelMixin = {
      *     Promise:
      *     A promise which resolves when the operation is complete.
      */
-    async destroy(options={}, context=undefined) {
+    async destroy(
+        options: Backbone.ModelDestroyOptions = {},
+        context: unknown = undefined,
+    ): Promise<void> {
         if (_.isFunction(options.success) ||
             _.isFunction(options.error) ||
             _.isFunction(options.complete)) {
             console.warn('RB.DraftResourceModelMixin.destroy was ' +
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
+
             return RB.promiseToCallbacks(
                 options, context, newOptions => this.destroy(newOptions));
         }
@@ -152,11 +174,13 @@ RB.DraftResourceModelMixin = {
      *     options (object):
      *         Options for the operation, including callbacks.
      */
-    _retrieveDraft(options) {
-        if (!RB.UserSession.instance.get('authenticated')) {
+    _retrieveDraft(
+        options: Backbone.PersistenceOptions,
+    ): Promise<void> {
+        if (!UserSession.instance.get('authenticated')) {
             return Promise.reject(new BackboneError(
                 this,
-                { errorText: gettext('You must be logged in to retrieve the draft.') },
+                { errorText: _`You must be logged in to retrieve the draft.` },
                 {}));
         }
 
@@ -167,31 +191,32 @@ RB.DraftResourceModelMixin = {
             data = _.extend({}, extraQueryArgs, data);
         }
 
-        return new Promise((resolve, reject) => {
+        return new Promise<void>((resolve, reject) => {
             Backbone.Model.prototype.fetch.call(this, {
                 data: data,
-                processData: true,
-                success: () => {
-                    /*
-                     * There was an existing draft, and we were redirected to it
-                     * and pulled data from it. We're done.
-                     */
-                    this._needDraft = false;
-
-                    resolve();
-                },
                 error: (model, xhr, options) => {
                     if (xhr.status === 404) {
                         /*
-                         * We now know we don't have an existing draft to work with,
-                         * and will eventually need to POST to create a new one.
+                         * We now know we don't have an existing draft to work
+                         * with, and will eventually need to POST to create a
+                         * new one.
                          */
                         this._needDraft = false;
                         resolve();
                     } else {
                         reject(new BackboneError(model, xhr, options));
                     }
-                }
+                },
+                processData: true,
+                success: () => {
+                    /*
+                     * There was an existing draft, and we were redirected to
+                     * it and pulled data from it. We're done.
+                     */
+                    this._needDraft = false;
+
+                    resolve();
+                },
             });
         });
     },
diff --git a/reviewboard/static/rb/js/resources/models/reviewReplyModel.es6.js b/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
similarity index 57%
rename from reviewboard/static/rb/js/resources/models/reviewReplyModel.es6.js
rename to reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
index ce258141cada06905e82bd82860e5adc51116db5..0e5ba244a05c9ec9362c4492815b9712b18adccc 100644
--- a/reviewboard/static/rb/js/resources/models/reviewReplyModel.es6.js
+++ b/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
@@ -1,106 +1,177 @@
 /**
  * A review reply.
+ */
+
+import { spina } from '@beanbag/spina';
+
+import { BaseResource, BaseResourceAttrs} from './baseResourceModel';
+import { DraftResourceModelMixin } from './draftResourceModelMixin';
+import { Review } from './reviewModel';
+import * as JSONSerializers from '../utils/serializers';
+
+
+/**
+ * Attributes for the ReviewReply model.
  *
- * Encapsulates replies to a top-level review.
- *
- * Model Attributes:
- *     forceTextType (string):
- *         The text type to request for text in all responses.
- *
- *     includeTextTypes (string):
- *         A comma-separated list of text types to include in responses.
- *
- *     rawTextFields (object):
- *         The contents of the raw text fields, if forceTextType is used and
- *         the caller fetches or posts with includeTextTypes=raw. The keys in
- *         this object are the field names, and the values are the raw versions
- *         of those attributes.
- *
- *     review (RB.Review):
- *         The review that this reply is replying to.
- *
- *     public (boolean):
- *         Whether this reply has been published.
- *
- *     bodyTop (string):
- *         The reply to the original review's ``bodyTop``.
- *
- *     bodyTopRichText (boolean):
- *         Whether the ``bodyTop`` field should be rendered as Markdown.
+ * Version Added:
+ *     6.0
+ */
+interface ReviewReplyAttrs extends BaseResourceAttrs {
+    /** The reply to the original review's ``bodyBottom``. */
+    bodyBottom: string;
+
+    /** Whether the ``bodyBottom`` field should be rendered as Markdown. */
+    bodyBottomRichText: boolean;
+
+    /** The reply to the original review's ``bodyTop``. */
+    bodyTop: string;
+
+    /** Whether the ``bodyTop`` field should be rendered as Markdown. */
+    bodyTopRichText: boolean;
+
+    /** The text type to request for text in all responses. */
+    forceTextType: string;
+
+    /** A comma-separated list of text types to include in responses. */
+    includeTextTypes: string;
+
+    /** Whether this reply has been published. */
+    public: boolean;
+
+    /**
+     * The contents of the raw text fields.
+     *
+     * This is set if ``forceTextType`` is used, and the caller fetches or
+     * posts with ``includeTextTypes=raw``. The keys in this object are the
+     * field names, and the values ar the raw versions of those attributes.
+     */
+    rawTextFields: { [key: string]: string };
+
+    /** The review being replied to. */
+    review: Review;
+
+    /** The timestamp of the reply. */
+    timestamp: string;
+}
+
+
+/**
+ * ReviewReply resource data returned by the server.
  *
- *     bodyBottom (string):
- *         The reply to the original review's ``bodyBottom``.
+ * Version Added:
+ *     6.0
+ */
+interface ReviewReplyResourceData {
+    body_bottom: string;
+    body_bottom_text_type: string;
+    body_top: string;
+    body_top_text_type: string;
+    force_text_type: string;
+    include_text_types: string;
+    raw_text_fields: { [key: string]: string };
+}
+
+
+/**
+ * Options for the publish operation.
  *
- *     bodyBottomRichText (boolean):
- *         Whether the ``bodyBottom`` field should be rendered as Markdown.
+ * Version Added:
+ *     6.0
+ */
+interface ReviewReplyPublishOptions extends Backbone.PersistenceOptions {
+    trivial?: boolean;
+}
+
+
+/**
+ * A review reply.
  *
- *     timestamp (string):
- *         The timestamp of this reply.
+ * Encapsulates replies to a top-level review.
  */
-RB.ReviewReply = RB.BaseResource.extend(_.extend({
-    defaults() {
+@spina({
+    mixins: [DraftResourceModelMixin],
+    prototypeAttrs: [
+        'COMMENT_LINK_NAMES',
+        'attrToJsonMap',
+        'deserializedAttrs',
+        'extraQueryArgs',
+        'listKey',
+        'rspNamespace',
+        'serializedAttrs',
+        'serializers',
+    ],
+})
+export class ReviewReply extends BaseResource<ReviewReplyAttrs> {
+    /**
+     * Return default values for the model attributes.
+     *
+     * Returns:
+     *     ReviewReplyAttrs:
+     *     The default attributes.
+     */
+    defaults(): ReviewReplyAttrs {
         return _.defaults({
+            bodyBottom: null,
+            bodyBottomRichText: false,
+            bodyTop: null,
+            bodyTopRichText: false,
             forceTextType: null,
             includeTextTypes: null,
+            'public': false,
             rawTextFields: {},
             review: null,
-            'public': false,
-            bodyTop: null,
-            bodyTopRichText: false,
-            bodyBottom: null,
-            bodyBottomRichText: false,
-            timestamp: null
-        }, RB.BaseResource.prototype.defaults());
-    },
+            timestamp: null,
+        }, super.defaults());
+    }
 
-    rspNamespace: 'reply',
-    listKey: 'replies',
+    static rspNamespace = 'reply';
+    static listKey = 'replies';
 
-    extraQueryArgs: {
+    static extraQueryArgs = {
         'force-text-type': 'html',
-        'include-text-types': 'raw'
-    },
+        'include-text-types': 'raw',
+    };
 
-    attrToJsonMap: {
+    static attrToJsonMap = {
         bodyBottom: 'body_bottom',
         bodyBottomRichText: 'body_bottom_text_type',
         bodyTop: 'body_top',
         bodyTopRichText: 'body_top_text_type',
         forceTextType: 'force_text_type',
-        includeTextTypes: 'include_text_types'
-    },
+        includeTextTypes: 'include_text_types',
+    };
 
-    serializedAttrs: [
-        'forceTextType',
-        'includeTextTypes',
-        'bodyTop',
-        'bodyTopRichText',
+    static serializedAttrs = [
         'bodyBottom',
         'bodyBottomRichText',
-        'public'
-    ],
-
-    deserializedAttrs: [
         'bodyTop',
+        'bodyTopRichText',
+        'forceTextType',
+        'includeTextTypes',
+        'public',
+    ];
+
+    static deserializedAttrs = [
         'bodyBottom',
+        'bodyTop',
         'public',
-        'timestamp'
-    ],
+        'timestamp',
+    ];
 
-    serializers: {
-        forceTextType: RB.JSONSerializers.onlyIfValue,
-        includeTextTypes: RB.JSONSerializers.onlyIfValue,
-        bodyTopRichText: RB.JSONSerializers.textType,
-        bodyBottomRichText: RB.JSONSerializers.textType,
-        'public': value => value ? true : undefined
-    },
+    static serializers = {
+        bodyBottomRichText: JSONSerializers.textType,
+        bodyTopRichText: JSONSerializers.textType,
+        forceTextType: JSONSerializers.onlyIfValue,
+        includeTextTypes: JSONSerializers.onlyIfValue,
+        'public': value => { return value ? true : undefined; },
+    };
 
-    COMMENT_LINK_NAMES: [
+    static COMMENT_LINK_NAMES = [
         'diff_comments',
         'file_attachment_comments',
         'general_comments',
-        'screenshot_comments'
-    ],
+        'screenshot_comments',
+    ];
 
     /**
      * Parse the response from the server.
@@ -113,10 +184,11 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
      *     object:
      *     The attribute values to set on the model.
      */
-    parseResourceData(rsp) {
+    parseResourceData(
+        rsp: ReviewReplyResourceData,
+    ): Partial<ReviewReplyAttrs> {
         const rawTextFields = rsp.raw_text_fields || rsp;
-        const data = RB.BaseResource.prototype.parseResourceData.call(
-            this, rsp);
+        const data = super.parseResourceData(rsp) as ReviewReplyAttrs;
 
         data.bodyTopRichText =
             (rawTextFields.body_top_text_type === 'markdown');
@@ -125,7 +197,7 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
         data.rawTextFields = rsp.raw_text_fields || {};
 
         return data;
-    },
+    }
 
     /**
      * Publish the reply.
@@ -148,13 +220,17 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
      *     Promise:
      *     A promise which resolves when the operation is complete.
      */
-    async publish(options={}, context=undefined) {
+    async publish(
+        options: ReviewReplyPublishOptions = {},
+        context: unknown = undefined,
+    ): Promise<void> {
         if (_.isFunction(options.success) ||
             _.isFunction(options.error) ||
             _.isFunction(options.complete)) {
             console.warn('RB.ReviewReply.publish was called using ' +
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
+
             return RB.promiseToCallbacks(options, context, newOptions =>
                 this.publish(newOptions));
         }
@@ -169,7 +245,7 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
             await this.save({
                 data: {
                     'public': 1,
-                    trivial: options.trivial ? 1 : 0
+                    trivial: options.trivial ? 1 : 0,
                 },
             });
         } catch (err) {
@@ -178,7 +254,7 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
         }
 
         this.trigger('published');
-    },
+    }
 
     /**
      * Discard the reply if it's empty.
@@ -202,13 +278,17 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
      *     A promise which resolves when the operation is complete. The
      *     resolution value will be true if discarded, false otherwise.
      */
-    async discardIfEmpty(options={}, context=undefined) {
+    async discardIfEmpty(
+        options: Backbone.PersistenceOptions = {},
+        context: unknown = undefined,
+    ): Promise<boolean> {
         if (_.isFunction(options.success) ||
             _.isFunction(options.error) ||
             _.isFunction(options.complete)) {
             console.warn('RB.ReviewReply.discardIfEmpty was called using ' +
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
+
             return RB.promiseToCallbacks(options, context, newOptions =>
                 this.discardIfEmpty(newOptions));
         }
@@ -220,7 +300,7 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
         } else {
             return this._checkCommentsLink(0);
         }
-    },
+    }
 
     /**
      * Check if there are comments, given the comment type.
@@ -241,26 +321,29 @@ RB.ReviewReply = RB.BaseResource.extend(_.extend({
      *     A promise which resolves when the operation is complete. The
      *     resolution value will be true if discarded, false otherwise.
      */
-    _checkCommentsLink(linkNameIndex) {
+    _checkCommentsLink(
+        linkNameIndex: number,
+    ): Promise<boolean> {
         return new Promise((resolve, reject) => {
-            const linkName = this.COMMENT_LINK_NAMES[linkNameIndex];
+            const linkName = ReviewReply.COMMENT_LINK_NAMES[linkNameIndex];
             const url = this.get('links')[linkName].href;
 
             RB.apiCall({
-                type: 'GET',
-                url: url,
+                error: (model, xhr, options) => reject(
+                    new BackboneError(model, xhr, options)),
                 success: rsp => {
                     if (rsp[linkName].length > 0) {
                         resolve(false);
-                    } else if (linkNameIndex < this.COMMENT_LINK_NAMES.length - 1) {
+                    } else if (linkNameIndex <
+                               ReviewReply.COMMENT_LINK_NAMES.length - 1) {
                         resolve(this._checkCommentsLink(linkNameIndex + 1));
                     } else {
                         resolve(this.destroy().then(() => true));
                     }
                 },
-                error: (model, xhr, options) => reject(
-                    new BackboneError(model, xhr, options)),
+                type: 'GET',
+                url: url,
             });
         });
     }
-}, RB.DraftResourceModelMixin));
+}
diff --git a/reviewboard/static/rb/js/resources/models/tests/reviewReplyModelTests.es6.js b/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
similarity index 97%
rename from reviewboard/static/rb/js/resources/models/tests/reviewReplyModelTests.es6.js
rename to reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
index 7211bf9ab6775b10e0065713c5ded2fea38dbdaa..e07ad2ccd6b6b5048c92407582ceefcf2d9a47c5 100644
--- a/reviewboard/static/rb/js/resources/models/tests/reviewReplyModelTests.es6.js
+++ b/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
@@ -1,25 +1,36 @@
+import {
+    beforeEach,
+    describe,
+    expect,
+    it,
+    spyOn,
+    suite,
+} from 'jasmine-core';
+
+import { BaseResource } from '../baseResourceModel';
+import { ReviewReply } from '../reviewReplyModel';
+
+
 suite('rb/resources/models/ReviewReply', function() {
     let parentObject;
     let model;
 
     beforeEach(function() {
-        parentObject = new RB.BaseResource({
-            'public': true,
+        parentObject = new BaseResource({
             links: {
                 replies: {
                     href: '/api/foos/replies/',
                 },
             },
+            'public': true,
         });
 
-        model = new RB.ReviewReply({
+        model = new ReviewReply({
             parentObject: parentObject,
         });
     });
 
     describe('destroy', function() {
-        let callbacks;
-
         beforeEach(function() {
             spyOn(Backbone.Model.prototype, 'destroy')
                 .and.callFake(options => options.success());
@@ -92,31 +103,31 @@ suite('rb/resources/models/ReviewReply', function() {
                 commentsData = {};
                 model.set({
                     id: 123,
-                    loaded: true,
                     links: {
-                        self: {
-                            href: '/api/foos/replies/123/',
-                        },
                         diff_comments: {
                             href: '/api/diff-comments/',
                         },
-                        screenshot_comments: {
-                            href: '/api/screenshot-comments/',
-                        },
                         file_attachment_comments: {
                             href: '/api/file-attachment-comments/',
                         },
                         general_comments: {
                             href: '/api/general-comments/',
                         },
+                        screenshot_comments: {
+                            href: '/api/screenshot-comments/',
+                        },
+                        self: {
+                            href: '/api/foos/replies/123/',
+                        },
                     },
+                    loaded: true,
                 });
 
                 spyOn(RB, 'apiCall').and.callFake(options => {
                     const links = model.get('links');
                     const data = {};
                     const key = _.find(
-                        RB.ReviewReply.prototype.COMMENT_LINK_NAMES,
+                        ReviewReply.COMMENT_LINK_NAMES,
                         name => (options.url === links[name].href));
 
                     if (key) {
@@ -205,6 +216,7 @@ suite('rb/resources/models/ReviewReply', function() {
                 spyOn(console, 'warn');
 
                 model.discardIfEmpty({
+                    error: () => done.fail(),
                     success: discarded => {
                         expect(discarded).toBe(true);
                         expect(model.destroy).toHaveBeenCalled();
@@ -212,7 +224,6 @@ suite('rb/resources/models/ReviewReply', function() {
 
                         done();
                     },
-                    error: () => done.fail(),
                 });
             });
         });
@@ -320,6 +331,7 @@ suite('rb/resources/models/ReviewReply', function() {
             spyOn(console, 'warn');
 
             model.ready({
+                error: () => done.fail(),
                 success: () => {
                     expect(parentObject.ready).toHaveBeenCalled();
                     expect(model._retrieveDraft).toHaveBeenCalled();
@@ -327,7 +339,6 @@ suite('rb/resources/models/ReviewReply', function() {
 
                     done();
                 },
-                error: () => done.fail(),
             });
         });
     });
@@ -338,16 +349,16 @@ suite('rb/resources/models/ReviewReply', function() {
         });
 
         it('API payloads', function() {
-            var data = model.parse({
-                stat: 'ok',
+            const data = model.parse({
                 my_reply: {
-                    id: 42,
-                    body_top: 'foo',
                     body_bottom: 'bar',
-                    'public': false,
-                    body_top_text_type: 'markdown',
                     body_bottom_text_type: 'plain',
+                    body_top: 'foo',
+                    body_top_text_type: 'markdown',
+                    id: 42,
+                    'public': false,
                 },
+                stat: 'ok',
             });
 
             expect(data).not.toBe(undefined);
diff --git a/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyDraftBannerView.ts b/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyDraftBannerView.ts
index 7998adf744bc4e1fe0436067b93656abe1673df6..5000f5aa16a9e7dadbebf9abee22ae0b170454d1 100644
--- a/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyDraftBannerView.ts
+++ b/reviewboard/static/rb/js/reviewRequestPage/views/reviewReplyDraftBannerView.ts
@@ -4,6 +4,9 @@ import {
     FloatingBannerView,
     FloatingBannerViewOptions,
 } from 'reviewboard/ui/views/floatingBannerView';
+import {
+    ReviewReply,
+} from 'reviewboard/common/resources/models/reviewReplyModel';
 import {
     ReviewRequestEditor,
 } from 'reviewboard/reviews/models/reviewRequestEditorModel';
@@ -30,7 +33,7 @@ interface ReviewReplyDraftBannerOptions extends FloatingBannerViewOptions {
  */
 @spina
 export class ReviewReplyDraftBannerView extends FloatingBannerView<
-    RB.ReviewReply,
+    ReviewReply,
     HTMLDivElement,
     ReviewReplyDraftBannerOptions
 > {
diff --git a/reviewboard/static/rb/js/reviews/models/unifiedBannerModel.ts b/reviewboard/static/rb/js/reviews/models/unifiedBannerModel.ts
index 1c1ab970ecc5835fa0a630b06997b00182d3aa97..dad2a1e52e86becbdb399add5172e9547c827179 100644
--- a/reviewboard/static/rb/js/reviews/models/unifiedBannerModel.ts
+++ b/reviewboard/static/rb/js/reviews/models/unifiedBannerModel.ts
@@ -4,6 +4,9 @@
 import { BaseModel, spina } from '@beanbag/spina';
 
 import { Review } from 'reviewboard/common/resources/models/reviewModel';
+import {
+    ReviewReply,
+} from 'reviewboard/common/resources/models/reviewReplyModel';
 
 import { ReviewRequestEditor } from './reviewRequestEditorModel';
 
@@ -116,14 +119,14 @@ export class UnifiedBanner extends BaseModel<UnifiedBannerAttrs> {
      * Update the draft state for the given review reply.
      *
      * Args:
-     *     reviewReply (RB.ReviewReply):
+     *     reviewReply (ReviewReply):
      *         The review reply model.
      *
      *     hasReviewReplyDraft (boolean):
      *          Whether the reviewReply passed in has a draft.
      */
     updateReplyDraftState(
-        reviewReply: RB.ReviewReply,
+        reviewReply: ReviewReply,
         hasReviewReplyDraft: boolean,
     ) {
         const reviewReplyDrafts = this.get('reviewReplyDrafts');
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 3a4b9127e716825dc94b7cd0e1b63693ec8072c2..360a5e2c9c2884124379afd8fba487bfe3aafaab 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -77,7 +77,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/resources/models/tests/repositoryBranchModelTests.es6.js',
             'rb/js/resources/models/tests/repositoryCommitModelTests.es6.js',
             'rb/js/resources/models/tests/reviewGroupModelTests.es6.js',
-            'rb/js/resources/models/tests/reviewReplyModelTests.es6.js',
             'rb/js/resources/models/tests/reviewRequestModelTests.es6.js',
             'rb/js/resources/models/tests/userFileAttachmentModelTests.es6.js',
             'rb/js/resources/models/tests/validateDiffModelTests.es6.js',
@@ -147,7 +146,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/resources/models/repositoryBranchModel.es6.js',
             'rb/js/resources/models/repositoryCommitModel.es6.js',
             'rb/js/resources/models/draftResourceChildModelMixin.es6.js',
-            'rb/js/resources/models/draftResourceModelMixin.es6.js',
             'rb/js/resources/models/draftReviewRequestModel.es6.js',
             'rb/js/resources/models/draftReviewModel.es6.js',
             'rb/js/resources/models/baseCommentModel.es6.js',
@@ -165,7 +163,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/resources/models/draftFileAttachmentModel.es6.js',
             'rb/js/resources/models/repositoryModel.es6.js',
             'rb/js/resources/models/reviewGroupModel.es6.js',
-            'rb/js/resources/models/reviewReplyModel.es6.js',
             'rb/js/resources/models/reviewRequestModel.es6.js',
             'rb/js/resources/models/screenshotModel.es6.js',
             'rb/js/resources/models/screenshotCommentModel.es6.js',
