diff --git a/rbgifcomments/rbgifcomments/__init__.py b/rbgifcomments/rbgifcomments/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..99e22cb14ca947e15b823b2c33df9580c1d6b298
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/__init__.py
@@ -0,0 +1,76 @@
+from __future__ import unicode_literals
+
+
+# The version of rbgifcomments
+#
+# This is in the format of:
+#
+#   (Major, Minor, Micro, Patch, alpha/beta/rc/final, Release Number, Released)
+#
+VERSION = (1, 0, 0, 0, 'rc', 1, False)
+
+
+def get_version_string():
+    """Returns the version number of this extension as a string.
+
+    Format looks like
+    (Major, Minor, Micro, Patch, alpha/beta/rc/final, Release Number, Released)
+
+    Returns:
+        str:
+        A string representing the format as described.
+    """
+    version = '%s.%s' % (VERSION[0], VERSION[1])
+
+    if VERSION[2] or VERSION[3]:
+        version += ".%s" % VERSION[2]
+
+    if VERSION[3]:
+        version += ".%s" % VERSION[3]
+
+    if VERSION[4] != 'final':
+        if VERSION[4] == 'rc':
+            version += ' RC%s' % VERSION[5]
+        else:
+            version += ' %s %s' % (VERSION[4], VERSION[5])
+
+    if not is_release():
+        version += ' (dev)'
+
+    return version
+
+
+def get_package_version():
+    """Returns the (numeric) version of this package.
+
+    Similar to get_version_string but without the (dev) or final/rc tags.
+
+    Returns:
+        str:
+        A string with the package version.
+    """
+    version = '%s.%s' % (VERSION[0], VERSION[1])
+
+    if VERSION[2] or VERSION[3]:
+        version += '.%s' % VERSION[2]
+
+    if VERSION[3]:
+        version += '.%s' % VERSION[3]
+
+    if VERSION[4] != 'final':
+        version += '%s%s' % (VERSION[4], VERSION[5])
+
+    return version
+
+
+def is_release():
+    """
+    Returns:
+        bool:
+        True if this is a released version of gifcomments
+    """
+    return VERSION[6]
+
+
+__version_info__ = VERSION[:-1]
+__version__ = get_package_version()
diff --git a/rbgifcomments/rbgifcomments/extension.py b/rbgifcomments/rbgifcomments/extension.py
new file mode 100644
index 0000000000000000000000000000000000000000..774d48a65e33c2df27fbc6ae2d8641ee2d7c5a3b
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/extension.py
@@ -0,0 +1,56 @@
+from __future__ import unicode_literals
+
+from django.contrib.staticfiles.templatetags.staticfiles import static
+from djblets.extensions.hooks import TemplateHook
+from reviewboard.extensions.base import Extension, JSExtension
+from reviewboard.urls import reviewable_url_names, review_request_url_names
+
+
+apply_to_url_names = set(reviewable_url_names + review_request_url_names)
+
+
+class GifCommentsJSExtension(JSExtension):
+    model_class = 'GifComments.Extension'
+    apply_to = apply_to_url_names
+
+
+class GifCommentsExtension(Extension):
+    """Extends Review Board with gifs from the webcam in comments.
+
+    When creating or updating comments, users will be allowed to record a gif
+    from their webcam. The gif has a (default) maximum length of 10 seconds
+    and a small resolution, to save space.
+    """
+
+    js_extensions = [GifCommentsJSExtension]
+
+    metadata = {
+        'Name': 'Gif Comments',
+    }
+
+    js_bundles = {
+        'gif-comments': {
+            'source_filenames': ['js/gifComments.es6.js',
+                                 'js/gifrecord.es6.js',
+                                 'js/lib/gif.js'],
+            'apply_to': apply_to_url_names,
+        }
+    }
+
+    css_bundles = {
+        'gif-comments': {
+            'source_filenames': ['css/gifcomments.less'],
+            'apply_to': apply_to_url_names,
+        }
+    }
+
+    def initialize(self):
+        """Initialize the extension."""
+        TemplateHook(self,
+                     'base-scripts-post',
+                     'rbgifcomments-workerpath.html',
+                     apply_to=apply_to_url_names)
+
+    @property
+    def js_worker_path(self):
+        return static('ext/%s/js/lib/gif.worker.js' % self.id)
diff --git a/rbgifcomments/rbgifcomments/static/css/gifcomments.less b/rbgifcomments/rbgifcomments/static/css/gifcomments.less
new file mode 100644
index 0000000000000000000000000000000000000000..834c2fc0c944e6eccf1a58fba9176ea99354c184
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/static/css/gifcomments.less
@@ -0,0 +1,16 @@
+.error-label {
+  color: #AA0000;
+}
+
+.gif-container {
+  text-align: center;
+  height: 100%;
+
+  input[type="button"]:not(:disabled) {
+    opacity: 0.99; // Buttons with opacity 1.0 become invisible on top of a video element in Chrome
+  }
+}
+
+.gifcomment-control-open {
+  float: right;
+}
diff --git a/rbgifcomments/rbgifcomments/static/js/gifComments.es6.js b/rbgifcomments/rbgifcomments/static/js/gifComments.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..d47e593fbaf68882409471901499841d84620f25
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/static/js/gifComments.es6.js
@@ -0,0 +1,194 @@
+const GIF_COMMENT_UI_STRINGS = {
+    OPEN_GIF_RECORD_UI: "Record a Gif",
+    CLOSE_GIF_RECORD_UI: "Cancel Gif",
+    DEFAULT_ERROR: "Oops. An error occurred :(",
+};
+
+const GifComments = {
+    isOpen: false,
+};
+
+GifComments.CommentDialogHookView = Backbone.View.extend({
+    initialize(options) {
+        this.commentDialog = options.commentDialog;
+        this.commentEditor = options.commentEditor;
+    },
+
+   /**
+    * Create the GIF recording UI if it does not exist.
+    *
+    * Create the GIF recording UI elements and insert it into the DOM into the
+    * appropriate spot. If the recording UI already is inserted, do nothing.
+    *
+    * Returns:
+    *     jQuery:
+    *     The newly created or previously existing DOM (div) element
+    *     that contains the recording UI.
+    */
+    injectUIIfNotInDOM() {
+        const $areaContainer = this.$('.comment-text-field .CodeMirror');
+
+        if ($areaContainer.length <= 0) {
+            console.error("Tried to create Gif recorder UI but could not find .CodeMirror " +
+                "container. Is markdown disabled?");
+
+            return null;
+        }
+
+        let $mainContainer = $('.gif-container', $areaContainer);
+
+        if ($mainContainer.length > 0) {
+            return $mainContainer;
+        }
+
+        const mainContainerHtml =
+            dedent`
+                <div class="gif-container">
+                   <div style="max-height:85%;">
+                       <span class="error-label" style="display:none;">${GIF_COMMENT_UI_STRINGS.DEFAULT_ERROR}</span>
+                       <img class="gif-preview" style="height:100%">
+                       <div class="loader" style="height:100%; display: none;">
+                           <div class="spinner"><span class="fa fa-spinner fa-pulse"></div>
+                       </div>
+                           <video class="gif-video" autoplay style="height:100%"></video>
+                       </div>
+                       <canvas class="gif-buffer-canvas" style="display:none">
+                       </canvas>
+                       <div>
+                           <input type="button" class="gif-ctrl-record" value="Record" disabled>
+                           <input type="button" class="gif-ctrl-stop" value="Stop" disabled>
+                           <input type="button" class="gif-ctrl-save" value="Save" disabled>
+                       </div>
+                   </div>
+                </div>
+            `;
+
+        $areaContainer.prepend(mainContainerHtml);
+        $mainContainer = $('.gif-container', $areaContainer);
+
+        // Find elements
+        const $canvasBuffer = $('.gif-buffer-canvas', $areaContainer);
+        const $previewImage = $('.gif-preview', $areaContainer);
+        const $previewVideo = $('.gif-video', $areaContainer);
+        const $startButton = $('.gif-ctrl-record', $areaContainer);
+        const $stopButton = $('.gif-ctrl-stop', $areaContainer);
+        const $saveButton = $('.gif-ctrl-save', $areaContainer);
+        const $errorLabel = $('.error-label', $areaContainer);
+        const $loader = $('.loader', $areaContainer);
+
+        // Bind events
+        $startButton.click(GifRecorder.startRecording.bind(GifRecorder));
+        $stopButton.click(GifRecorder.stopRecording.bind(GifRecorder));
+        $saveButton.click(ev => {
+            const gif = GifRecorder.gifBlob;
+            gif.lastModifiedDate = new Date();
+            gif.name = "RecordedGif.gif";
+            this.commentDialog._textEditor._uploadImage(gif);
+            this.closeGifComment();
+        });
+
+        // Hook up GifRecorder to DOM
+        GifRecorder.$previewVideo = $previewVideo[0];
+        GifRecorder.$previewImage = $previewImage[0];
+        GifRecorder.$canvasBuffer = $canvasBuffer[0];
+        GifRecorder.$startRecButton = $startButton[0];
+        GifRecorder.$stopRecButton = $stopButton[0];
+        GifRecorder.$saveButton = $saveButton[0];
+        GifRecorder.$errorLabel = $errorLabel[0];
+        GifRecorder.$loader = $loader[0];
+
+        return $mainContainer;
+    },
+
+    /**
+     * Display and initialize the Gif recorder.
+     *
+     * This will (via GifRecorder) begin the recording workflow
+     * (i.e. ask user for camera permissions, etc)
+     */
+    openGifComment() {
+        this.$mainContainer.show();
+        this.$toggleMainContainerButton.val(GIF_COMMENT_UI_STRINGS.CLOSE_GIF_RECORD_UI);
+        GifRecorder.initialize();
+    },
+
+    /**
+     * Close (if visible) and shut down the gif recorder.
+     *
+     * This will (via GifRecorder) end the recording media stream
+     * (i.e. browser will no longer say camera in use)
+     */
+    closeGifComment() {
+        if (this.$mainContainer) {
+            this.$mainContainer.hide();
+        }
+
+        if (this.$toggleMainContainerButton) {
+            this.$toggleMainContainerButton.val(GIF_COMMENT_UI_STRINGS.OPEN_GIF_RECORD_UI);
+        }
+
+        GifRecorder.shutdown();
+    },
+
+    /**
+     * Inject the Record a Gif button to start the Gif recorder.
+     *
+     * Also hook up events to a few other related buttons (like enable markdown
+     * checkbox and cancel comment button) to shutdown gif recording.
+     */
+    render() {
+        const $buttons = this.$('.comment-issue-options');
+
+        this.$toggleMainContainerButton =
+            $(`<input class="gifcomment-control-open" type="button"
+               value="${GIF_COMMENT_UI_STRINGS.OPEN_GIF_RECORD_UI}">`);
+        $buttons.append(this.$toggleMainContainerButton);
+
+        this.$toggleMainContainerButton.click(ev => {
+            this.$mainContainer = this.injectUIIfNotInDOM();
+
+            if (!this.$mainContainer || this.$mainContainer.length <= 0) {
+                return;
+            }
+
+            GifComments.isOpen = !GifComments.isOpen;
+
+            if (GifComments.isOpen) {
+                this.openGifComment();
+            } else {
+                this.closeGifComment();
+            }
+        });
+
+        const $enableMarkdownCheckbox = this.$('#enable_markdown');
+        $enableMarkdownCheckbox.change(() => {
+            if (!$enableMarkdownCheckbox[0].checked) {
+                GifComments.isOpen = false;
+                this.closeGifComment();
+            }
+            this.$toggleMainContainerButton
+                .prop('disabled', !$enableMarkdownCheckbox.prop('checked'));
+        });
+
+        this.$("input.cancel").click(ev => {
+            GifComments.isOpen = false;
+            this.closeGifComment();
+        });
+    },
+});
+
+
+GifComments.Extension = RB.Extension.extend({
+    /**
+     * Initialize the GifComments extension by adding a hook to the comment
+     * dialog.
+     */
+    initialize() {
+        RB.Extension.prototype.initialize.call(this);
+
+        this._commentDialogHook = new RB.CommentDialogHook({
+            extension: this,
+            viewType: GifComments.CommentDialogHookView,
+        });
+    },
+});
\ No newline at end of file
diff --git a/rbgifcomments/rbgifcomments/static/js/gifrecord.es6.js b/rbgifcomments/rbgifcomments/static/js/gifrecord.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..930e5d51b66a7cb71b1d41dd5dbf15eeb7465a43
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/static/js/gifrecord.es6.js
@@ -0,0 +1,425 @@
+const GIF_WIDTH = 320;
+const GIF_HEIGHT = 240;
+const GIF_MAX_LENGTH_SECONDS = 10;
+
+const VIDEO_CONFIG = {
+    width: GIF_WIDTH,
+    height: GIF_HEIGHT
+};
+
+const FPS = 10;
+const FPS_DELAY = 1000 / FPS; // Delay between frames in ms.
+
+const GifEncoderAbstraction = {
+    workerScriptPath: null,
+
+    /**
+     * Create a new gif encoder.
+     *
+     * The created gif encoder is initialized as "blank" and ready
+     * for frame additions.
+     *
+     * Args:
+     *     width (number):
+     *         The width in pixels of the gif.
+     *
+     *     height (number):
+     *         The height in pixels of the gif.
+     *
+     * Returns:
+     *     GIF:
+     *     Returns a GIF encoder from the gif.js library.
+     */
+    createGif(width, height) {
+        const workerScriptPath = this.workerScriptPath;
+
+        if (!workerScriptPath) {
+            console.error("GifWorker script path has not been set.");
+        }
+
+        return new GIF({
+            workers: 2,
+            workerScript: workerScriptPath,
+            quality: 30,
+            dither: 'FloydSteinberg',
+            width: width,
+            height: height,
+        });
+    },
+
+    /**
+     * Add a frame into the Gif encoder.
+     *
+     * Copy the current frame in the canvas (from the context) to the Gif.
+     * This does not encode it until renderGifIntoBlob is called.
+     *
+     * Args:
+     *     canvasContext (CanvasRenderingContext2D):
+     *         The canvas context to sample from.
+     *
+     *     gif (GIF):
+     *         The GIF encoder to sample into.
+     *
+     *     delay (number):
+     *         The delay, in milliseconds, that this frame
+     *         will be played after the previous.
+     */
+    sampleFrameIntoGif(canvasContext, gif, delay) {
+        gif.addFrame(canvasContext, {delay: delay, copy: true});
+    },
+
+    /**
+     * Render the GIF encoder into an actual gif.
+     *
+     * The encoding will happen asynchronously (on the worker) and will
+     * call the callback function when the final blob is ready, passing it as
+     * the argument.
+     *
+     * Args:
+     *     gif (GIF):
+     *         The GIF encoder.
+     *
+     *     blobCallback (function):
+     *         A function to call when he final blob is ready. Should take single
+     *         parameter of type Blob.
+     */
+    renderGifIntoBlob(gif, blobCallback) {
+        gif.on('finished', blobCallback);
+        gif.render();
+    },
+};
+
+const GifRecorder = {
+    intervalIds: {
+        frameSampler: -1,
+        timer: -1,
+    },
+    gif: null,
+    gifBlob: null,
+    isRecording: false,
+    isDead: false, // Used for handling certain async callbacks that can
+                   // happen after the UI has been closed
+    $previewVideo: null,
+    $previewImage: null,
+    $startRecButton: null,
+    $stopRecButton: null,
+    $saveButton: null,
+    $canvasBuffer: null,
+    $errorLabel: null,
+    $loader: null,
+    recordingTimeRemaining: 0,
+    stream: null,
+
+    /**
+     * Check for navigator.mediaDevices.getUserMedia.
+     *
+     * Return:
+     *     boolean:
+     *         Returns true if this browser has the getUserMedia function.
+     */
+    hasGetUserMedia() {
+        return !!(navigator.mediaDevices.getUserMedia);
+    },
+
+    /**
+     * Display an (HTML) error message.
+     *
+     * Args:
+     *     error (string):
+     *         HTML to display.
+     */
+    displayError(error) {
+        this.$errorLabel.style.display = null;
+        this.$errorLabel.innerHTML = error;
+
+    },
+
+    /**
+     * Initialize the Gif recording backend.
+     *
+     * Mainly do setup things, and start the "workflow" by requesting
+     * camera permissions from the user.
+     */
+    initialize() {
+        this.isDead = false;
+        this.$errorLabel.style.display = 'none';
+        this.$loader.style.display = 'none';
+
+        if (!this.hasGetUserMedia()) {
+            this.displayError("Your browser is not currently supported. Sorry!");
+            return;
+        }
+
+        // Workflow is: getCameraPermission ->
+        // (when permission granted) displayMediaStream ->
+        // (when media stream ready) displayRecordingControls
+
+        this.getCameraPermission();
+    },
+
+    /**
+     * Shutdown Gif recording backend.
+     *
+     * Stop streams, relinquish camera permissions, and invoke the
+     * DOM to update.
+     */
+    shutdown() {
+        this.isDead = true;
+        if (this.stream !== null) {
+            this.stream.getVideoTracks().forEach(t => t.stop());
+            this.stream = null;
+        }
+
+        if (this.isRecording) {
+            this.isRecording = false;
+
+            this.updateRecordingControlsRecordingCompleted();
+
+            // Stop sampler
+            clearInterval(this.intervalIds.frameSampler);
+            clearInterval(this.intervalIds.timer);
+        }
+    },
+
+    /**
+     * Triage and display a media error.
+     *
+     * By Triage, we mean check for a few types of known errors and convert
+     * their messages to something more user-friendly. Throws the original
+     * error into the console log for debugging.
+     *
+     * Args:
+     *     e (Error):
+     *         An error object to triage and display.
+     */
+    displayMediaStreamError(e) {
+        if (e.name === 'NotAllowedError' || e.name === 'PermissionDeniedError') {
+
+            this.displayError(
+                dedent`
+                    It appears you've blocked your browser from accessing
+                    your webcam.
+                    <br/><br/>
+                    Look for a crossed out camera icon near the website
+                    address above, and click it to re-grant access.
+                    <br/>
+                    Then try pressing Record a Gif again.
+                `);
+        } else {
+            this.displayError(e.message);
+        }
+
+        console.error(error);
+    },
+
+    /**
+     * Pop up the browser camera permission dialog.
+     *
+     * This method is the beginning of the gif workflow.
+     * It will ask for camera permission from the user and
+     * then invoke the appropriate callback later.
+     */
+    getCameraPermission() {
+        navigator.mediaDevices.getUserMedia({
+            video: VIDEO_CONFIG,
+            audio: false,
+        })
+            .then(this.displayMediaStream.bind(this))
+            .catch(this.displayMediaStreamError.bind(this));
+    },
+
+    /**
+     * Render a media stream.
+     *
+     * Render the given media stream to the $previewVideo video element.
+     *
+     * Args:
+     *     stream (MediaStream):
+     *         A media stream from the webcam.
+     */
+    displayMediaStream(stream) {
+        this.stream = stream;
+
+        // User may have clicked Cancel Gif after the permissions dialog got shown
+        // and then clicked allow on the permissions, causing us to be here.
+        if (this.isDead) {
+            this.shutdown();
+            return;
+        }
+
+        this.$previewVideo.onloadedmetadata = () => {
+            this.$startRecButton.disabled = false;
+        };
+
+        try {
+            this.$previewVideo.srcObject = stream;
+        } catch (error) {
+            this.$previewVideo.src = URL.createObjectURL(stream);
+        }
+
+        this.$previewVideo.style.display = null;
+        this.$previewImage.style.display = 'none';
+    },
+
+    /**
+     * Enable controls to start recording.
+     */
+    updateRecordingControlsRecordingAvailable() {
+        this.$stopRecButton.disabled = true;
+        this.$startRecButton.disabled = false;
+        this.$startRecButton.value = "Record";
+        this.$saveButton.disabled = !this.gifBlob;
+    },
+
+    /**
+     * Enable controls to stop recording.
+     */
+    updateRecordingControlsRecordingInProgress() {
+        this.$stopRecButton.disabled = false;
+        this.$startRecButton.disabled = true;
+        this.$saveButton.disabled = true;
+    },
+
+    /**
+     * Disable all controls for processing the recorded file.
+     */
+    updateRecordingControlsRecordingCompleted() {
+        this.$stopRecButton.disabled = true;
+        this.$startRecButton.disabled = true;
+        this.$saveButton.disabled = true;
+    },
+
+    /**
+     * Update the time display.
+     *
+     * Displays the seconds remaining to record.
+     * Args:
+     *     time (number):
+     *         Time in seconds to display.
+     */
+    updateRecordTimeRemaining(time) {
+        this.$startRecButton.value = time;
+    },
+
+    /**
+     * Start recording a gif.
+     *
+     * This will update the controls, and start sampling frames.
+     */
+    startRecording() {
+        if (this.isRecording) {
+            return;
+        }
+
+        this.updateRecordingControlsRecordingInProgress();
+
+        // Setup canvas
+        this.$canvasBuffer.width = GIF_WIDTH;
+        this.$canvasBuffer.height = GIF_HEIGHT;
+
+        let context = this.$canvasBuffer.getContext('2d');
+
+        // Setup gif container
+        this.gif = GifEncoderAbstraction.createGif(GIF_WIDTH, GIF_HEIGHT);
+
+        this.recordTimeRemaining = GIF_MAX_LENGTH_SECONDS + 1;
+
+        // Start the sampling loop
+        this.intervalIds.frameSampler = setInterval(
+            this.gifFrameHandler.bind(this, context, GIF_WIDTH, GIF_HEIGHT,
+                FPS_DELAY, this.$previewVideo),
+            FPS_DELAY
+        );
+
+        this.intervalIds.timer = setInterval(
+            this.recordingTimerTick.bind(this), 1000);
+
+        this.recordingTimerTick();
+
+        this.$previewVideo.style.display = null;
+        this.$previewImage.style.display = 'none';
+
+        this.isRecording = true;
+    },
+
+    /**
+     * Stop recording a gif.
+     *
+     * This will update the controls, stop sampling, and start rendering.
+     */
+    stopRecording() {
+        if (!this.isRecording) {
+            return;
+        }
+
+        this.isRecording = false;
+
+        this.updateRecordingControlsRecordingCompleted();
+
+        // Stop sampler
+        clearInterval(this.intervalIds.frameSampler);
+        clearInterval(this.intervalIds.timer);
+
+        this.intervalIds.frameSampler = -1;
+        this.intervalIds.timer = -1;
+
+        this.$previewImage.style.display = null;
+        this.$previewVideo.style.display = 'none';
+
+        this.$loader.style.display = null;
+
+        GifEncoderAbstraction.renderGifIntoBlob(this.gif, (blob) => {
+            this.gifBlob = blob;
+            this.$previewImage.src = URL.createObjectURL(blob);
+            this.$saveButton.disabled = false;
+            this.$loader.style.display = 'none';
+            this.updateRecordingControlsRecordingAvailable();
+        });
+    },
+
+    /**
+     * Callback function for the recording timer.
+     *
+     * This should tick every second to track the time recording,
+     * and update the UI accordingly.
+     */
+    recordingTimerTick() {
+        this.recordTimeRemaining--;
+
+        this.updateRecordTimeRemaining(this.recordTimeRemaining);
+
+        if (this.recordTimeRemaining <= 0) {
+            this.stopRecording();
+        }
+    },
+
+    /**
+     * Callback function for the frame sampler.
+     *
+     * This will sample the video element into the canvas context and then
+     * transfer that into the Gif encoder.
+     *
+     * Args:
+     *     context (Canvas):
+     *         Context to use as a buffer.
+     *
+     *     width (number):
+     *         The width in pixels of the frame to sample.
+     *
+     *     height (number):
+     *         The height in pixels of the frame to sample.
+     *
+     *     delay (number):
+     *         The delay, in milliseconds, that this frame will be played after the previous.
+     *
+     *     video (HTMLElement):
+     *         The video element to sample a frame from.
+     */
+    gifFrameHandler(context, width, height, delay, video) {
+        // Sampling is done by drawing video to (canvas) context, then copying
+        // that into the gif frame.
+
+        context.drawImage(video, 0, 0, width, height);
+        GifEncoderAbstraction.sampleFrameIntoGif(context, this.gif, delay);
+    },
+};
\ No newline at end of file
diff --git a/rbgifcomments/rbgifcomments/templates/rbgifcomments-workerpath.html b/rbgifcomments/rbgifcomments/templates/rbgifcomments-workerpath.html
new file mode 100644
index 0000000000000000000000000000000000000000..da2fe4f3350023abcbdc0a076083680ca3e8b7a5
--- /dev/null
+++ b/rbgifcomments/rbgifcomments/templates/rbgifcomments-workerpath.html
@@ -0,0 +1,3 @@
+<script type="text/javascript">
+GifEncoderAbstraction.workerScriptPath = '{{extension.js_worker_path|safe}}';
+</script>
diff --git a/rbgifcomments/setup.py b/rbgifcomments/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..195d99e11249966764fa32991697420ff2ce6cdd
--- /dev/null
+++ b/rbgifcomments/setup.py
@@ -0,0 +1,40 @@
+from __future__ import unicode_literals
+
+from reviewboard.extensions.packaging import setup
+
+from rbgifcomments import get_package_version
+
+
+setup(
+    name='rbgifcomments',
+    version=get_package_version(),
+    description='Record gifs from your webcam and attach them '
+                'to Review Board comments!',
+    url='https://www.reviewboard.org/',
+    author='Roman Polyanovsky',
+    author_email='rpolyano@gmail.com',
+    maintainer='Roman Polyanovsky',
+    maintainer_email='rpolyano@gmail.com',
+    packages=[b'rbgifcomments'],
+    entry_points={
+        'reviewboard.extensions': [
+            'rbgifcomments = rbgifcomments.extension:GifCommentsExtension',
+        ]
+    },
+    package_data={
+        b'rbgifcomments': [
+            'templates/*.html',
+            'templates/*.txt',
+        ],
+    },
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Review Board',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: English',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ]
+)
