Refactor directory structure of components

There is no change in functionality. Only moving things around.

+ Separate html from the js.
+ Place the unit test for a component within the same folder.
+ Organize the components in subfolders.

Change-Id: I51fdc510db75fc1b33f040ca63decbbdfd4d5513
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
new file mode 100644
index 0000000..caa9674
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -0,0 +1,89 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/gr-change-list-styles.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+
+<dom-module id="gr-change-list-item">
+  <template>
+    <style>
+      :host {
+        display: flex;
+        border-bottom: 1px solid #eee;
+      }
+      :host([selected]) {
+        background-color: #ebf5fb;
+      }
+      :host([needs-review]) {
+        font-weight: bold;
+      }
+      .cell {
+        flex-shrink: 0;
+        padding: .3em .5em;
+      }
+      a {
+        color: var(--default-text-color);
+        text-decoration: none;
+      }
+      a:hover {
+        text-decoration: underline;
+      }
+      .positionIndicator {
+        visibility: hidden;
+      }
+      :host([selected]) .positionIndicator {
+        visibility: visible;
+      }
+      .u-monospace {
+        font-family: var(--monospace-font-family);
+      }
+      .u-green {
+        color: #388E3C;
+      }
+      .u-red {
+        color: #D32F2F;
+      }
+    </style>
+    <style include="gr-change-list-styles"></style>
+    <span class="cell keyboard">
+      <span class="positionIndicator">&#x25b6;</span>
+    </span>
+    <span class="cell star" hidden$="[[!showStar]]">
+      <gr-change-star change="{{change}}"></gr-change-star>
+    </span>
+    <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
+    <span class="cell status">[[_computeChangeStatusString(change)]]</span>
+    <span class="cell owner">
+      <gr-account-link account="[[change.owner]]"></gr-account-link>
+    </span>
+    <a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
+    <a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
+    <gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter>
+    <span class="cell size u-monospace">
+      <span class="u-green"><span>+</span>[[change.insertions]]</span>,
+      <span class="u-red"><span>-</span>[[change.deletions]]</span>
+    </span>
+    <template is="dom-repeat" items="[[labelNames]]" as="labelName">
+      <span title$="[[_computeLabelTitle(change, labelName)]]"
+          class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span>
+    </template>
+  </template>
+  <script src="gr-change-list-item.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
new file mode 100644
index 0000000..5d03cba
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -0,0 +1,132 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-change-list-item',
+
+    properties: {
+      selected: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      needsReview: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      labelNames: {
+        type: Array,
+      },
+      change: Object,
+      changeURL: {
+        type: String,
+        computed: '_computeChangeURL(change._number)',
+      },
+      showStar: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    _computeChangeURL: function(changeNum) {
+      if (!changeNum) { return ''; }
+      return '/c/' + changeNum + '/';
+    },
+
+    _computeChangeStatusString: function(change) {
+      if (change.status == this.ChangeStatus.MERGED) {
+        return 'Merged';
+      }
+      if (change.mergeable != null && change.mergeable == false) {
+        return 'Merge Conflict';
+      }
+      if (change.status == this.ChangeStatus.DRAFT) {
+        return 'Draft';
+      }
+      if (change.status == this.ChangeStatus.ABANDONED) {
+        return 'Abandoned';
+      }
+      return '';
+    },
+
+    _computeLabelTitle: function(change, labelName) {
+      var label = change.labels[labelName];
+      if (!label) { return labelName; }
+      var significantLabel = label.rejected || label.approved ||
+          label.disliked || label.recommended;
+      if (significantLabel && significantLabel.name) {
+        return labelName + '\nby ' + significantLabel.name;
+      }
+      return labelName;
+    },
+
+    _computeLabelClass: function(change, labelName) {
+      var label = change.labels[labelName];
+      // Mimic a Set.
+      var classes = {
+        'cell': true,
+        'label': true,
+      };
+      if (label) {
+        if (label.approved) {
+          classes['u-green'] = true;
+        }
+        if (label.value == 1) {
+          classes['u-monospace'] = true;
+          classes['u-green'] = true;
+        } else if (label.value == -1) {
+          classes['u-monospace'] = true;
+          classes['u-red'] = true;
+        }
+        if (label.rejected) {
+          classes['u-red'] = true;
+        }
+      }
+      return Object.keys(classes).sort().join(' ');
+    },
+
+    _computeLabelValue: function(change, labelName) {
+      var label = change.labels[labelName];
+      if (!label) { return ''; }
+      if (label.approved) {
+        return '✓';
+      }
+      if (label.rejected) {
+        return '✕';
+      }
+      if (label.value > 0) {
+        return '+' + label.value;
+      }
+      if (label.value < 0) {
+        return label.value;
+      }
+      return '';
+    },
+
+    _computeProjectURL: function(project) {
+      return '/projects/' + project + ',dashboards/default';
+    },
+
+    _computeProjectBranchURL: function(project, branch) {
+      return '/q/status:open+project:' + project + '+branch:' + branch;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-list-item-test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-change-list-item-test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 5f06945..0a4aec4 100644
--- a/polygerrit-ui/app/test/gr-change-list-item-test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list-item</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../elements/gr-change-list-item.html">
+<link rel="import" href="gr-change-list-item.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
new file mode 100644
index 0000000..5b03274
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -0,0 +1,91 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../gr-change-list/gr-change-list.html">
+
+<dom-module id="gr-change-list-view">
+  <template>
+    <style>
+      :host {
+        background-color: var(--view-background-color);
+        display: block;
+        margin: 0 var(--default-horizontal-margin);
+      }
+      .loading,
+      .error {
+        margin-top: 1em;
+        background-color: #f1f2f3;
+      }
+      .loading {
+        color: #666;
+      }
+      .error {
+        color: #D32F2F;
+      }
+      gr-change-list {
+        margin-top: 1em;
+        width: 100%;
+      }
+      nav {
+        margin-bottom: 1em;
+        padding: .5em 0;
+        text-align: center;
+      }
+      nav a {
+        display: inline-block;
+      }
+      nav a:first-of-type {
+        margin-right: .5em;
+      }
+      @media only screen and (max-width: 50em) {
+        :host {
+          margin: 0;
+        }
+        .loading,
+        .error {
+          padding: 0 var(--default-horizontal-margin);
+        }
+      }
+    </style>
+    <gr-ajax
+        auto
+        url="/changes/"
+        params="[[_computeQueryParams(_query, _offset)]]"
+        last-response="{{_changes}}"
+        last-error="{{_lastError}}"
+        loading="{{_loading}}"></gr-ajax>
+    <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
+    <div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden>
+      [[_lastError.request.xhr.responseText]]
+    </div>
+    <div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden>
+      <gr-change-list
+          changes="{{_changes}}"
+          selected-index="{{viewState.selectedChangeIndex}}"
+          show-star="[[loggedIn]]"></gr-change-list>
+      <nav>
+        <a href$="[[_computeNavLink(_query, _offset, -1)]]"
+           hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
+        <a href$="[[_computeNavLink(_query, _offset, 1)]]"
+           hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
+      </nav>
+    </div>
+  </template>
+  <script src="gr-change-list-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
new file mode 100644
index 0000000..d0a97c1d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -0,0 +1,149 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var DEFAULT_NUM_CHANGES = 25;
+
+  Polymer({
+    is: 'gr-change-list-view',
+
+    /**
+     * Fired when the title of the page should change.
+     *
+     * @event title-change
+     */
+
+    properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+
+      /**
+       * True when user is logged in.
+       */
+      loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * State persisted across restamps of the element.
+       */
+      viewState: {
+        type: Object,
+        notify: true,
+        value: function() { return {}; },
+      },
+
+      /**
+       * Currently active query.
+       */
+      _query: String,
+
+      /**
+       * Offset of currently visible query results.
+       */
+      _offset: Number,
+
+      /**
+       * Change objects loaded from the server.
+       */
+      _changes: Array,
+
+      /**
+       * Contains error of last request (in case of change loading error).
+       */
+      _lastError: Object,
+
+      /**
+       * For showing a "loading..." string during ajax requests.
+       */
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    attached: function() {
+      this.fire('title-change', {title: this._query});
+    },
+
+    _paramsChanged: function(value) {
+      if (value.view != this.tagName.toLowerCase()) { return; }
+
+      this._query = value.query;
+      this._offset = value.offset || 0;
+      if (this.viewState.query != this._query ||
+          this.viewState.offset != this._offset) {
+        this.set('viewState.selectedChangeIndex', 0);
+        this.set('viewState.query', this._query);
+        this.set('viewState.offset', this._offset);
+      }
+
+      this.fire('title-change', {title: this._query});
+    },
+
+    _computeQueryParams: function(query, offset) {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.LABELS,
+          this.ListChangesOption.DETAILED_ACCOUNTS
+      );
+      var obj = {
+        n: DEFAULT_NUM_CHANGES,  // Number of results to return.
+        O: options,
+        S: offset || 0,
+      };
+      if (query && query.length > 0) {
+        obj.q = query;
+      }
+      return obj;
+    },
+
+    _computeNavLink: function(query, offset, direction) {
+      // Offset could be a string when passed from the router.
+      offset = +(offset || 0);
+      var newOffset = Math.max(0, offset + (25 * direction));
+      var href = '/q/' + query;
+      if (newOffset > 0) {
+        href += ',' + newOffset;
+      }
+      return href;
+    },
+
+    _computeErrorHidden: function(loading, lastError) {
+      return loading || lastError == null;
+    },
+
+    _computeListHidden: function(loading, lastError) {
+      return loading || lastError != null;
+    },
+
+    _hidePrevArrow: function(offset) {
+      return offset == 0;
+    },
+
+    _hideNextArrow: function(changesLen) {
+      return changesLen < DEFAULT_NUM_CHANGES;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
new file mode 100644
index 0000000..8ff66cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -0,0 +1,66 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../styles/gr-change-list-styles.html">
+<link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
+
+<dom-module id="gr-change-list">
+  <template>
+    <style>
+      :host {
+        display: flex;
+        flex-direction: column;
+      }
+    </style>
+    <style include="gr-change-list-styles"></style>
+    <div class="headerRow">
+      <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
+      <span class="topHeader star" hidden$="[[!showStar]]"></span>
+      <span class="topHeader subject">Subject</span>
+      <span class="topHeader status">Status</span>
+      <span class="topHeader owner">Owner</span>
+      <span class="topHeader project">Project</span>
+      <span class="topHeader branch">Branch</span>
+      <span class="topHeader updated">Updated</span>
+      <span class="topHeader size">Size</span>
+      <template is="dom-repeat" items="[[labelNames]]" as="labelName">
+        <span class="topHeader label" title$="[[labelName]]">
+          [[_computeLabelShortcut(labelName)]]
+        </span>
+      </template>
+    </div>
+    <template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex">
+      <template is="dom-if" if="[[_groupTitle(groupIndex)]]">
+        <div class="groupHeader">[[_groupTitle(groupIndex)]]</div>
+      </template>
+      <template is="dom-if" if="[[!changeGroup.length]]">
+        <div class="noChanges">No changes</div>
+      </template>
+      <template is="dom-repeat" items="[[changeGroup]]" as="change">
+        <gr-change-list-item
+            selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
+            needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
+            change="[[change]]"
+            show-star="[[showStar]]"
+            label-names="[[labelNames]]"></gr-change-list-item>
+      </template>
+    </template>
+  </template>
+  <script src="gr-change-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
new file mode 100644
index 0000000..ef71be3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -0,0 +1,165 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-change-list',
+
+    hostAttributes: {
+      tabindex: 0,
+    },
+
+    properties: {
+      /**
+       * The logged-in user's account, or an empty object if no user is logged
+       * in.
+       */
+      account: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      /**
+       * An array of ChangeInfo objects to render.
+       * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+       */
+      changes: {
+        type: Array,
+        observer: '_changesChanged',
+      },
+      /**
+       * ChangeInfo objects grouped into arrays. The groups and changes
+       * properties should not be used together.
+       */
+      groups: {
+        type: Array,
+        value: function() { return []; },
+      },
+      groupTitles: {
+        type: Array,
+        value: function() { return []; },
+      },
+      labelNames: {
+        type: Array,
+        computed: '_computeLabelNames(groups)',
+      },
+      selectedIndex: {
+        type: Number,
+        notify: true,
+      },
+      showStar: {
+        type: Boolean,
+        value: false,
+      },
+      showReviewedState: {
+        type: Boolean,
+        value: false,
+      },
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+      Gerrit.RESTClientBehavior,
+    ],
+
+    _computeLabelNames: function(groups) {
+      if (!groups) { return []; }
+      var labels = [];
+      var nonExistingLabel = function(item) {
+        return labels.indexOf(item) < 0;
+      };
+      for (var i = 0; i < groups.length; i++) {
+        var group = groups[i];
+        for (var j = 0; j < group.length; j++) {
+          var change = group[j];
+          if (!change.labels) { continue; }
+          var currentLabels = Object.keys(change.labels);
+          labels = labels.concat(currentLabels.filter(nonExistingLabel));
+        }
+      }
+      return labels.sort();
+    },
+
+    _computeLabelShortcut: function(labelName) {
+      return labelName.replace(/[a-z-]/g, '');
+    },
+
+    _changesChanged: function(changes) {
+      this.groups = changes ? [changes] : [];
+    },
+
+    _groupTitle: function(groupIndex) {
+      if (groupIndex > this.groupTitles.length - 1) { return null; }
+      return this.groupTitles[groupIndex];
+    },
+
+    _computeItemSelected: function(index, groupIndex, selectedIndex) {
+      var idx = 0;
+      for (var i = 0; i < groupIndex; i++) {
+        idx += this.groups[i].length;
+      }
+      idx += index;
+      return idx == selectedIndex;
+    },
+
+    _computeItemNeedsReview: function(account, change, showReviewedState) {
+      return showReviewedState && !change.reviewed &&
+          change.status != this.ChangeStatus.MERGED &&
+          account._account_id != change.owner._account_id;
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+
+      if (this.groups == null) { return; }
+      var len = 0;
+      this.groups.forEach(function(group) {
+        len += group.length;
+      });
+      switch (e.keyCode) {
+        case 74:  // 'j'
+          e.preventDefault();
+          if (this.selectedIndex == len - 1) { return; }
+          this.selectedIndex += 1;
+          break;
+        case 75:  // 'k'
+          e.preventDefault();
+          if (this.selectedIndex == 0) { return; }
+          this.selectedIndex -= 1;
+          break;
+        case 79:  // 'o'
+        case 13:  // 'enter'
+          e.preventDefault();
+          page.show(this._changeURLForIndex(this.selectedIndex));
+          break;
+      }
+    },
+
+    _changeURLForIndex: function(index) {
+      var changeEls = this._getListItems();
+      if (index < changeEls.length && changeEls[index]) {
+        return changeEls[index].changeURL;
+      }
+      return '';
+    },
+
+    _getListItems: function() {
+      return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-list-test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-change-list-test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index edd2ab8..0575f85 100644
--- a/polygerrit-ui/app/test/gr-change-list-test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -18,14 +18,14 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-list.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
new file mode 100644
index 0000000..e351f44
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -0,0 +1,64 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+
+<dom-module id="gr-dashboard-view">
+  <template>
+    <style>
+      :host {
+        background-color: var(--view-background-color);
+        display: block;
+        margin: 0 var(--default-horizontal-margin);
+      }
+      .loading {
+        margin-top: 1em;
+        color: #666;
+        background-color: #f1f2f3;
+      }
+      gr-change-list {
+        margin-top: 1em;
+        width: 100%;
+      }
+      @media only screen and (max-width: 50em) {
+        :host {
+          margin: 0;
+        }
+        .loading {
+          padding: 0 var(--default-horizontal-margin);
+        }
+      }
+    </style>
+    <gr-ajax
+        auto
+        url="/changes/"
+        params="[[_computeQueryParams()]]"
+        last-response="{{_results}}"
+        loading="{{_loading}}"></gr-ajax>
+    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+    <div hidden$="[[_loading]]" hidden>
+      <gr-change-list
+          show-star
+          show-reviewed-state
+          account="[[account]]"
+          selected-index="{{viewState.selectedChangeIndex}}"
+          groups="{{_results}}"
+          group-titles="[[_groupTitles]]"></gr-change-list>
+    </div>
+  </template>
+  <script src="gr-dashboard-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
new file mode 100644
index 0000000..fc6a3ff
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -0,0 +1,76 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-dashboard-view',
+
+    /**
+     * Fired when the title of the page should change.
+     *
+     * @event title-change
+     */
+
+    properties: {
+      account: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      viewState: Object,
+
+      _results: Array,
+      _groupTitles: {
+        type: Array,
+        value: [
+          'Outgoing reviews',
+          'Incoming reviews',
+          'Recently closed',
+        ],
+      },
+
+      /**
+       * For showing a "loading..." string during ajax requests.
+       */
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    attached: function() {
+      this.fire('title-change', {title: 'My Reviews'});
+    },
+
+    _computeQueryParams: function() {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.LABELS,
+          this.ListChangesOption.DETAILED_ACCOUNTS,
+          this.ListChangesOption.REVIEWED
+      );
+      return {
+        O: options,
+        q: [
+          'is:open owner:self',
+          'is:open reviewer:self -owner:self',
+          'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
+        ],
+      };
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
new file mode 100644
index 0000000..1593fab
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -0,0 +1,84 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
+
+<dom-module id="gr-change-actions">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      gr-button {
+        display: block;
+        margin-bottom: .5em;
+      }
+      gr-button:before {
+        content: attr(data-label);
+      }
+      gr-button[loading]:before {
+        content: attr(data-loading-label);
+      }
+      @media screen and (max-width: 50em) {
+        .confirmDialog {
+          width: 90vw;
+        }
+      }
+    </style>
+    <gr-ajax id="actionsXHR"
+        url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
+        last-response="{{_revisionActions}}"
+        loading="{{_loading}}"></gr-ajax>
+    <div>
+      <template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
+        <gr-button title$="[[action.title]]"
+            primary$="[[_computePrimary(action.__key)]]"
+            hidden$="[[!action.enabled]]"
+            data-action-key$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            on-tap="_handleActionTap"></gr-button>
+      </template>
+      <template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
+        <gr-button title$="[[action.title]]"
+            primary$="[[_computePrimary(action.__key)]]"
+            disabled$="[[!action.enabled]]"
+            data-action-key$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
+            on-tap="_handleActionTap"></gr-button>
+      </template>
+    </div>
+    <gr-overlay id="overlay" with-backdrop>
+      <gr-confirm-rebase-dialog id="confirmRebase"
+          class="confirmDialog"
+          on-confirm="_handleRebaseConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          hidden></gr-confirm-rebase-dialog>
+    </gr-overlay>
+  </template>
+  <script src="gr-change-actions.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
new file mode 100644
index 0000000..a89c0aa
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -0,0 +1,225 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  // TODO(davido): Add the rest of the change actions.
+  var ChangeActions = {
+    ABANDON: 'abandon',
+    DELETE: '/',
+    RESTORE: 'restore',
+  };
+
+  // TODO(andybons): Add the rest of the revision actions.
+  var RevisionActions = {
+    DELETE: '/',
+    PUBLISH: 'publish',
+    REBASE: 'rebase',
+    SUBMIT: 'submit',
+  };
+
+  Polymer({
+    is: 'gr-change-actions',
+
+    /**
+     * Fired when the change should be reloaded.
+     *
+     * @event reload-change
+     */
+
+    properties: {
+      actions: {
+        type: Object,
+      },
+      changeNum: String,
+      patchNum: String,
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _revisionActions: Object,
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    observers: [
+      '_actionsChanged(actions, _revisionActions)',
+    ],
+
+    reload: function() {
+      if (!this.changeNum || !this.patchNum) {
+        return Promise.resolve();
+      }
+      return this.$.actionsXHR.generateRequest().completes;
+    },
+
+    _actionsChanged: function(actions, revisionActions) {
+      this.hidden =
+          revisionActions.rebase == null &&
+          revisionActions.submit == null &&
+          revisionActions.publish == null &&
+          actions.abandon == null &&
+          actions.restore == null;
+    },
+
+    _computeRevisionActionsPath: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/actions';
+    },
+
+    _getValuesFor: function(obj) {
+      return Object.keys(obj).map(function(key) {
+        return obj[key];
+      });
+    },
+
+    _computeActionValues: function(actions, type) {
+      var result = [];
+      var values = this._getValuesFor(
+          type == 'change' ? ChangeActions : RevisionActions);
+      for (var a in actions) {
+        if (values.indexOf(a) == -1) { continue; }
+        actions[a].__key = a;
+        actions[a].__type = type;
+        result.push(actions[a]);
+      }
+      return result;
+    },
+
+    _computeLoadingLabel: function(action) {
+      return {
+        'rebase': 'Rebasing...',
+        'submit': 'Submitting...',
+      }[action];
+    },
+
+    _computePrimary: function(actionKey) {
+      return actionKey == 'submit';
+    },
+
+    _computeButtonClass: function(action) {
+      if ([RevisionActions.SUBMIT,
+          RevisionActions.PUBLISH].indexOf(action) != -1) {
+        return 'primary';
+      }
+      return '';
+    },
+
+    _handleActionTap: function(e) {
+      e.preventDefault();
+      var el = Polymer.dom(e).rootTarget;
+      var key = el.getAttribute('data-action-key');
+      var type = el.getAttribute('data-action-type');
+      if (type == 'revision') {
+        if (key == RevisionActions.REBASE) {
+          this._showRebaseDialog();
+          return;
+        }
+        this._fireRevisionAction(this._prependSlash(key),
+            this._revisionActions[key]);
+      } else {
+        this._fireChangeAction(this._prependSlash(key), this.actions[key]);
+      }
+    },
+
+    _prependSlash: function(key) {
+      return key == '/' ? key : '/' + key;
+    },
+
+    _handleConfirmDialogCancel: function() {
+      var dialogEls =
+          Polymer.dom(this.root).querySelectorAll('.confirmDialog');
+      for (var i = 0; i < dialogEls.length; i++) {
+        dialogEls[i].hidden = true;
+      }
+      this.$.overlay.close();
+    },
+
+    _handleRebaseConfirm: function() {
+      var payload = {};
+      var el = this.$.confirmRebase;
+      if (el.clearParent) {
+        // There is a subtle but important difference between setting the base
+        // to an empty string and omitting it entirely from the payload. An
+        // empty string implies that the parent should be cleared and the
+        // change should be rebased on top of the target branch. Leaving out
+        // the base implies that it should be rebased on top of its current
+        // parent.
+        payload.base = '';
+      } else if (el.base && el.base.length > 0) {
+        payload.base = el.base;
+      }
+      this.$.overlay.close();
+      el.hidden = false;
+      this._fireRevisionAction('/rebase', this._revisionActions.rebase,
+          payload);
+    },
+
+    _fireChangeAction: function(endpoint, action) {
+      this._send(action.method, {}, endpoint).then(
+        function() {
+          // We can’t reload a change that was deleted.
+          if (endpoint == ChangeActions.DELETE) {
+            page.show('/');
+          } else {
+            this.fire('reload-change', null, {bubbles: false});
+          }
+        }.bind(this)).catch(function(err) {
+          alert('Oops. Something went wrong. Check the console and bug the ' +
+              'PolyGerrit team for assistance.');
+          throw err;
+        });
+    },
+
+    _fireRevisionAction: function(endpoint, action, opt_payload) {
+      var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
+      buttonEl.setAttribute('loading', true);
+      buttonEl.disabled = true;
+      function enableButton() {
+        buttonEl.removeAttribute('loading');
+        buttonEl.disabled = false;
+      }
+
+      this._send(action.method, opt_payload, endpoint, true).then(
+        function() {
+          this.fire('reload-change', null, {bubbles: false});
+          enableButton();
+        }.bind(this)).catch(function(err) {
+          // TODO(andybons): Handle merge conflict (409 status);
+          alert('Oops. Something went wrong. Check the console and bug the ' +
+              'PolyGerrit team for assistance.');
+          enableButton();
+          throw err;
+        });
+    },
+
+    _showRebaseDialog: function() {
+      this.$.confirmRebase.hidden = false;
+      this.$.overlay.open();
+    },
+
+    _send: function(method, payload, actionEndpoint, revisionAction) {
+      var xhr = document.createElement('gr-request');
+      this._xhrPromise = xhr.send({
+        method: method,
+        url: this.changeBaseURL(this.changeNum,
+            revisionAction ? this.patchNum : null) + actionEndpoint,
+        body: payload,
+      });
+
+      return this._xhrPromise;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-actions-test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
similarity index 91%
rename from polygerrit-ui/app/test/gr-change-actions-test.html
rename to polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index e510e34..a89a7a5 100644
--- a/polygerrit-ui/app/test/gr-change-actions-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-actions</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-actions.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-actions.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
similarity index 62%
rename from polygerrit-ui/app/elements/gr-change-metadata.html
rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 81b580f..7b1a2f1 100644
--- a/polygerrit-ui/app/elements/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,13 +14,13 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-account-link.html">
-<link rel="import" href="gr-date-formatter.html">
-<link rel="import" href="gr-reviewer-list.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
 
-<script src="../scripts/fake-app.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
 
 <dom-module id="gr-change-metadata">
   <template>
@@ -123,69 +123,5 @@
       </section>
     </template>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var SubmitTypeLabel = {
-      FAST_FORWARD_ONLY: 'Fast Forward Only',
-      MERGE_IF_NECESSARY: 'Merge if Necessary',
-      REBASE_IF_NECESSARY: 'Rebase if Necessary',
-      MERGE_ALWAYS: 'Always Merge',
-      CHERRY_PICK: 'Cherry Pick',
-    };
-
-    Polymer({
-      is: 'gr-change-metadata',
-
-      properties: {
-        change: Object,
-        mutable: Boolean,
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      _computeHideStrategy: function(change) {
-        var open = change.status == this.ChangeStatus.NEW ||
-            change.status == this.ChangeStatus.DRAFT;
-        return !open;
-      },
-
-      _computeStrategy: function(change) {
-        return SubmitTypeLabel[change.submit_type];
-      },
-
-      _computeLabelNames: function(labels) {
-        return Object.keys(labels).sort();
-      },
-
-      _computeLabelValues: function(labelName, labels) {
-        var result = [];
-        var t = labels[labelName];
-        if (!t) { return result; }
-        var approvals = t.all || [];
-        approvals.forEach(function(label) {
-          if (label.value && label.value != labels[labelName].default_value) {
-            var labelClassName;
-            var labelValPrefix = '';
-            if (label.value > 0) {
-              labelValPrefix = '+';
-              labelClassName = 'approved';
-            } else if (label.value < 0) {
-              labelClassName = 'notApproved';
-            }
-            result.push({
-              value: labelValPrefix + label.value,
-              className: labelClassName,
-              account: label,
-            });
-          }
-        });
-        return result;
-      },
-    });
-  })();
-  </script>
+  <script src="gr-change-metadata.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
new file mode 100644
index 0000000..3d2633b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -0,0 +1,76 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var SubmitTypeLabel = {
+    FAST_FORWARD_ONLY: 'Fast Forward Only',
+    MERGE_IF_NECESSARY: 'Merge if Necessary',
+    REBASE_IF_NECESSARY: 'Rebase if Necessary',
+    MERGE_ALWAYS: 'Always Merge',
+    CHERRY_PICK: 'Cherry Pick',
+  };
+
+  Polymer({
+    is: 'gr-change-metadata',
+
+    properties: {
+      change: Object,
+      mutable: Boolean,
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    _computeHideStrategy: function(change) {
+      var open = change.status == this.ChangeStatus.NEW ||
+          change.status == this.ChangeStatus.DRAFT;
+      return !open;
+    },
+
+    _computeStrategy: function(change) {
+      return SubmitTypeLabel[change.submit_type];
+    },
+
+    _computeLabelNames: function(labels) {
+      return Object.keys(labels).sort();
+    },
+
+    _computeLabelValues: function(labelName, labels) {
+      var result = [];
+      var t = labels[labelName];
+      if (!t) { return result; }
+      var approvals = t.all || [];
+      approvals.forEach(function(label) {
+        if (label.value && label.value != labels[labelName].default_value) {
+          var labelClassName;
+          var labelValPrefix = '';
+          if (label.value > 0) {
+            labelValPrefix = '+';
+            labelClassName = 'approved';
+          } else if (label.value < 0) {
+            labelClassName = 'notApproved';
+          }
+          result.push({
+            value: labelValPrefix + label.value,
+            className: labelClassName,
+            account: label,
+          });
+        }
+      });
+      return result;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-metadata-test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
similarity index 82%
rename from polygerrit-ui/app/test/gr-change-metadata-test.html
rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index e4ce22f..6c97b5a 100644
--- a/polygerrit-ui/app/test/gr-change-metadata-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-metadata</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-metadata.html">
-<script src="../scripts/util.js"></script>
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-metadata.html">
+<script src="../../../scripts/util.js"></script>
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
new file mode 100644
index 0000000..41cb058
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -0,0 +1,319 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+
+<link rel="import" href="../gr-change-actions/gr-change-actions.html">
+<link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
+<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
+<link rel="import" href="../gr-file-list/gr-file-list.html">
+<link rel="import" href="../gr-messages-list/gr-messages-list.html">
+<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
+<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
+
+<dom-module id="gr-change-view">
+  <template>
+    <style>
+      .container {
+        margin: 1em var(--default-horizontal-margin);
+      }
+      .container:not(.loading) {
+        background-color: var(--view-background-color);
+      }
+      .container.loading {
+        color: #666;
+      }
+      .headerContainer {
+        height: 4.1em;
+        margin-bottom: .5em;
+      }
+      .header {
+        align-items: center;
+        background-color: var(--view-background-color);
+        border-bottom: 1px solid #ddd;
+        display: flex;
+        padding: 1em var(--default-horizontal-margin);
+        z-index: 99;  /* Less than gr-overlay's backdrop */
+      }
+      .header.pinned {
+        border-bottom-color: transparent;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+        position: fixed;
+        top: 0;
+        transition: box-shadow 250ms linear;
+        width: calc(100% - (2 * var(--default-horizontal-margin)));
+      }
+      .header-title {
+        flex: 1;
+        font-size: 1.2em;
+        font-weight: bold;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      gr-change-star {
+        margin-right: .25em;
+        vertical-align: -.425em;
+      }
+      .download,
+      .patchSelectLabel {
+        margin-left: var(--default-horizontal-margin);
+      }
+      .header select {
+        margin-left: .5em;
+      }
+      .header .reply {
+        margin-left: var(--default-horizontal-margin);
+      }
+      gr-reply-dialog {
+        min-width: 30em;
+        max-width: 50em;
+      }
+      .changeStatus {
+        color: #999;
+        text-transform: capitalize;
+      }
+      section {
+        margin: 10px 0;
+        padding: 10px var(--default-horizontal-margin);
+      }
+      /* Strong specificity here is needed due to
+         https://github.com/Polymer/polymer/issues/2531 */
+      .container section.changeInfo {
+        border-bottom: 1px solid #ddd;
+        display: flex;
+        margin-top: 0;
+        padding-top: 0;
+      }
+      .changeInfo-column:not(:last-of-type) {
+        margin-right: 1em;
+        padding-right: 1em;
+      }
+      .changeMetadata {
+        border-right: 1px solid #ddd;
+        font-size: .9em;
+      }
+      gr-change-actions {
+        margin-top: 1em;
+      }
+      .commitMessage {
+        font-family: var(--monospace-font-family);
+        flex: 0 0 72ch;
+        margin-right: 2em;
+        margin-bottom: 1em;
+      }
+      .commitMessage h4 {
+        font-family: var(--font-family);
+        font-weight: bold;
+        margin-bottom: .25em;
+      }
+      .commitAndRelated {
+        align-content: flex-start;
+        display: flex;
+        flex: 1;
+        flex-wrap: wrap;
+      }
+      gr-file-list {
+        margin-bottom: 1em;
+        padding: 0 var(--default-horizontal-margin);
+      }
+      @media screen and (max-width: 50em) {
+        .container {
+          margin: .5em 0 !important;
+        }
+        .container.loading {
+          margin: 1em var(--default-horizontal-margin) !important;
+        }
+        .headerContainer {
+          height: 5.15em;
+        }
+        .header {
+          align-items: flex-start;
+          flex-direction: column;
+          padding: .5em var(--default-horizontal-margin) !important;
+        }
+        gr-change-star {
+          vertical-align: middle;
+        }
+        .header-title,
+        .header-actions,
+        .header.pinned {
+          width: 100% !important;
+        }
+        .header-title {
+          font-size: 1.1em;
+        }
+        .header-actions {
+          align-items: center;
+          display: flex;
+          justify-content: space-between;
+          margin-top: .5em;
+        }
+        gr-reply-dialog {
+          min-width: initial;
+          width: 90vw;
+        }
+        .download {
+          display: none;
+        }
+        .patchSelectLabel {
+          margin-left: 0 !important;
+          margin-right: .5em;
+        }
+        .header select {
+          margin-left: 0 !important;
+          margin-right: .5em;
+        }
+        .header .reply {
+          margin-left: 0 !important;
+          margin-right: .5em;
+        }
+        .changeInfo-column:not(:last-of-type) {
+          margin-right: 0;
+          padding-right: 0;
+        }
+        .changeInfo,
+        .commitAndRelated {
+          flex-direction: column;
+          flex-wrap: nowrap;
+        }
+        .changeMetadata {
+          font-size: 1em;
+          border-right: none;
+          margin-bottom: 1em;
+          margin-top: .25em;
+          max-width: none;
+        }
+        .commitMessage {
+          flex: initial;
+          margin-right: 0;
+        }
+      }
+    </style>
+    <gr-ajax id="detailXHR"
+        url="[[_computeDetailPath(_changeNum)]]"
+        params="[[_computeDetailQueryParams()]]"
+        last-response="{{_change}}"
+        loading="{{_loading}}"></gr-ajax>
+    <gr-ajax id="commentsXHR"
+        url="[[_computeCommentsPath(_changeNum)]]"
+        last-response="{{_comments}}"></gr-ajax>
+    <gr-ajax id="commitInfoXHR"
+        url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]"
+        last-response="{{_commitInfo}}"></gr-ajax>
+    <!-- TODO(andybons): Cache the project config. -->
+    <gr-ajax id="configXHR"
+        auto
+        url="[[_computeProjectConfigPath(_change.project)]]"
+        last-response="{{_projectConfig}}"></gr-ajax>
+    <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
+    <div class="container" hidden$="{{_loading}}">
+      <div class="headerContainer">
+        <div class="header">
+          <span class="header-title">
+            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
+            <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
+            <span>[[_change.subject]]</span>
+            <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
+          </span>
+          <span class="header-actions">
+            <gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button>
+            <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
+            <span>
+              <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
+              <select id="patchSetSelect" on-change="_handlePatchChange">
+                <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
+                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
+                    <span>[[patchNumber]]</span>
+                    /
+                    <span>[[_computeLatestPatchNum(_change)]]</span>
+                  </option>
+                </template>
+              </select>
+            </span>
+          </span>
+        </div>
+      </div>
+      <section class="changeInfo">
+        <div class="changeInfo-column changeMetadata">
+          <gr-change-metadata
+              change="[[_change]]"
+              mutable="[[_loggedIn]]"></gr-change-metadata>
+          <gr-change-actions id="actions"
+              actions="[[_change.actions]]"
+              change-num="[[_changeNum]]"
+              patch-num="[[_patchNum]]"
+              on-reload-change="_handleReloadChange"></gr-change-actions>
+        </div>
+        <div class="changeInfo-column commitAndRelated">
+          <div class="commitMessage">
+            <h4>Commit message</h4>
+            <gr-linked-text pre
+                content="[[_commitInfo.message]]"
+                config="[[_projectConfig.commentlinks]]"></gr-linked-text>
+          </div>
+          <div class="relatedChanges">
+            <gr-related-changes-list id="relatedChanges"
+              change="[[_change]]"
+              server-config="[[serverConfig]]"
+              patch-num="[[_patchNum]]"></gr-related-changes-list>
+          </div>
+        </div>
+      </section>
+      <gr-file-list id="fileList"
+          change-num="[[_changeNum]]"
+          patch-num="[[_patchNum]]"
+          comments="[[_comments]]"
+          selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+      <gr-messages-list id="messageList"
+          change-num="[[_changeNum]]"
+          messages="[[_change.messages]]"
+          comments="[[_comments]]"
+          project-config="[[_projectConfig]]"
+          show-reply-buttons="[[_loggedIn]]"
+          on-reply="_handleMessageReply"></gr-messages-list>
+    </div>
+    <gr-overlay id="downloadOverlay" with-backdrop>
+      <gr-download-dialog
+          change="[[_change]]"
+          patch-num="[[_patchNum]]"
+          config="[[serverConfig.download]]"
+          on-close="_handleDownloadDialogClose"></gr-download-dialog>
+    </gr-overlay>
+    <gr-overlay id="replyOverlay"
+        on-iron-overlay-opened="_handleReplyOverlayOpen"
+        with-backdrop>
+      <gr-reply-dialog id="replyDialog"
+          change-num="[[_changeNum]]"
+          patch-num="[[_patchNum]]"
+          labels="[[_change.labels]]"
+          permitted-labels="[[_change.permitted_labels]]"
+          on-send="_handleReplySent"
+          on-cancel="_handleReplyCancel"
+          hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
+    </gr-overlay>
+  </template>
+  <script src="gr-change-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
new file mode 100644
index 0000000..a42a379
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -0,0 +1,354 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-change-view',
+
+    /**
+     * Fired when the title of the page should change.
+     *
+     * @event title-change
+     */
+
+    properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+      viewState: {
+        type: Object,
+        notify: true,
+        value: function() { return {}; },
+      },
+      serverConfig: Object,
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+
+      _comments: Object,
+      _change: {
+        type: Object,
+        observer: '_changeChanged',
+      },
+      _commitInfo: Object,
+      _changeNum: String,
+      _patchNum: String,
+      _allPatchSets: {
+        type: Array,
+        computed: '_computeAllPatchSets(_change)',
+      },
+      _loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+      _loading: Boolean,
+      _headerContainerEl: Object,
+      _headerEl: Object,
+      _projectConfig: Object,
+      _boundScrollHandler: {
+        type: Function,
+        value: function() { return this._handleBodyScroll.bind(this); },
+      },
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+      Gerrit.RESTClientBehavior,
+    ],
+
+    ready: function() {
+      app.accountReady.then(function() {
+        this._loggedIn = app.loggedIn;
+      }.bind(this));
+      this._headerEl = this.$$('.header');
+    },
+
+    attached: function() {
+      window.addEventListener('scroll', this._boundScrollHandler);
+    },
+
+    detached: function() {
+      window.removeEventListener('scroll', this._boundScrollHandler);
+    },
+
+    _handleBodyScroll: function(e) {
+      var containerEl = this._headerContainerEl ||
+          this.$$('.headerContainer');
+
+      // Calculate where the header is relative to the window.
+      var top = containerEl.offsetTop;
+      for (var offsetParent = containerEl.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+      // The element may not be displayed yet, in which case do nothing.
+      if (top == 0) { return; }
+
+      this._headerEl.classList.toggle('pinned', window.scrollY >= top);
+    },
+
+    _resetHeaderEl: function() {
+      var el = this._headerEl || this.$$('.header');
+      this._headerEl = el;
+      el.classList.remove('pinned');
+    },
+
+    _handlePatchChange: function(e) {
+      var patchNum = e.target.value;
+      var currentPatchNum =
+          this._change.revisions[this._change.current_revision]._number;
+      if (patchNum == currentPatchNum) {
+        page.show(this._computeChangePath(this._changeNum));
+        return;
+      }
+      page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
+    },
+
+    _handleReplyTap: function(e) {
+      e.preventDefault();
+      this.$.replyOverlay.open();
+    },
+
+    _handleDownloadTap: function(e) {
+      e.preventDefault();
+      this.$.downloadOverlay.open();
+    },
+
+    _handleDownloadDialogClose: function(e) {
+      this.$.downloadOverlay.close();
+    },
+
+    _handleMessageReply: function(e) {
+      var msg = e.detail.message.message;
+      var quoteStr = msg.split('\n').map(
+          function(line) { return '> ' + line; }).join('\n') + '\n\n';
+      this.$.replyDialog.draft += quoteStr;
+      this.$.replyOverlay.open();
+    },
+
+    _handleReplyOverlayOpen: function(e) {
+      this.$.replyDialog.reload().then(function() {
+        this.async(function() { this.$.replyOverlay.center() }, 1);
+      }.bind(this));
+      this.$.replyDialog.focus();
+    },
+
+    _handleReplySent: function(e) {
+      this.$.replyOverlay.close();
+      this._reload();
+    },
+
+    _handleReplyCancel: function(e) {
+      this.$.replyOverlay.close();
+    },
+
+    _paramsChanged: function(value) {
+      if (value.view != this.tagName.toLowerCase()) { return; }
+
+      this._changeNum = value.changeNum;
+      this._patchNum = value.patchNum;
+      if (this.viewState.changeNum != this._changeNum ||
+          this.viewState.patchNum != this._patchNum) {
+        this.set('viewState.selectedFileIndex', 0);
+        this.set('viewState.changeNum', this._changeNum);
+        this.set('viewState.patchNum', this._patchNum);
+      }
+      if (!this._changeNum) {
+        return;
+      }
+      this._reload().then(function() {
+        this.$.messageList.topMargin = this._headerEl.offsetHeight;
+
+        // Allow the message list to render before scrolling.
+        this.async(function() {
+          var msgPrefix = '#message-';
+          var hash = window.location.hash;
+          if (hash.indexOf(msgPrefix) == 0) {
+            this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
+          }
+        }.bind(this), 1);
+
+        app.accountReady.then(function() {
+          if (!this._loggedIn) { return; }
+
+          if (this.viewState.showReplyDialog) {
+            this.$.replyOverlay.open();
+            this.set('viewState.showReplyDialog', false);
+          }
+        }.bind(this));
+      }.bind(this));
+    },
+
+    _changeChanged: function(change) {
+      if (!change) { return; }
+      this._patchNum = this._patchNum ||
+          change.revisions[change.current_revision]._number;
+
+      var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+      this.fire('title-change', {title: title});
+    },
+
+    _computeChangePath: function(changeNum) {
+      return '/c/' + changeNum;
+    },
+
+    _computeChangePermalink: function(changeNum) {
+      return '/' + changeNum;
+    },
+
+    _computeChangeStatus: function(change, patchNum) {
+      var status = change.status;
+      if (status == this.ChangeStatus.NEW) {
+        var rev = this._getRevisionNumber(change, patchNum);
+        // TODO(davido): Figure out, why sometimes revision is not there
+        if (rev == undefined || !rev.draft) { return ''; }
+        status = this.ChangeStatus.DRAFT;
+      }
+      return '(' + status.toLowerCase() + ')';
+    },
+
+    _computeDetailPath: function(changeNum) {
+      return '/changes/' + changeNum + '/detail';
+    },
+
+    _computeCommitInfoPath: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/commit?links';
+    },
+
+    _computeCommentsPath: function(changeNum) {
+      return '/changes/' + changeNum + '/comments';
+    },
+
+    _computeProjectConfigPath: function(project) {
+      return '/projects/' + encodeURIComponent(project) + '/config';
+    },
+
+    _computeDetailQueryParams: function() {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.ALL_REVISIONS,
+          this.ListChangesOption.CHANGE_ACTIONS,
+          this.ListChangesOption.DOWNLOAD_COMMANDS
+      );
+      return {O: options};
+    },
+
+    _computeLatestPatchNum: function(change) {
+      return change.revisions[change.current_revision]._number;
+    },
+
+    _computeAllPatchSets: function(change) {
+      var patchNums = [];
+      for (var rev in change.revisions) {
+        patchNums.push(change.revisions[rev]._number);
+      }
+      return patchNums.sort(function(a, b) {
+        return a - b;
+      });
+    },
+
+    _getRevisionNumber: function(change, patchNum) {
+      for (var rev in change.revisions) {
+        if (change.revisions[rev]._number == patchNum) {
+          return change.revisions[rev];
+        }
+      }
+    },
+
+    _computePatchIndexIsSelected: function(index, patchNum) {
+      return this._allPatchSets[index] == patchNum;
+    },
+
+    _computeLabelNames: function(labels) {
+      return Object.keys(labels).sort();
+    },
+
+    _computeLabelValues: function(labelName, labels) {
+      var result = [];
+      var t = labels[labelName];
+      if (!t) { return result; }
+      var approvals = t.all || [];
+      approvals.forEach(function(label) {
+        if (label.value && label.value != labels[labelName].default_value) {
+          var labelClassName;
+          var labelValPrefix = '';
+          if (label.value > 0) {
+            labelValPrefix = '+';
+            labelClassName = 'approved';
+          } else if (label.value < 0) {
+            labelClassName = 'notApproved';
+          }
+          result.push({
+            value: labelValPrefix + label.value,
+            className: labelClassName,
+            account: label,
+          });
+        }
+      });
+      return result;
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+
+      switch (e.keyCode) {
+        case 65:  // 'a'
+          e.preventDefault();
+          this.$.replyOverlay.open();
+          break;
+        case 85:  // 'u'
+          e.preventDefault();
+          page.show('/');
+          break;
+      }
+    },
+
+    _handleReloadChange: function() {
+      page.show(this._computeChangePath(this._changeNum));
+    },
+
+    _reload: function() {
+      var detailCompletes = this.$.detailXHR.generateRequest().completes;
+      this.$.commentsXHR.generateRequest();
+      var reloadPatchNumDependentResources = function() {
+        return Promise.all([
+          this.$.commitInfoXHR.generateRequest().completes,
+          this.$.actions.reload(),
+          this.$.fileList.reload(),
+        ]);
+      }.bind(this);
+      var reloadDetailDependentResources = function() {
+        return this.$.relatedChanges.reload();
+      }.bind(this);
+
+      this._resetHeaderEl();
+
+      if (this._patchNum) {
+        return reloadPatchNumDependentResources().then(function() {
+          return detailCompletes;
+        }).then(reloadDetailDependentResources);
+      } else {
+        // The patch number is reliant on the change detail request.
+        return detailCompletes.then(reloadPatchNumDependentResources).then(
+            reloadDetailDependentResources);
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-view-test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
similarity index 90%
rename from polygerrit-ui/app/test/gr-change-view-test.html
rename to polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index f8ab059..ed9d28d 100644
--- a/polygerrit-ui/app/test/gr-change-view-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -18,14 +18,14 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-view</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-view.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-view.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
new file mode 100644
index 0000000..263fb28
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -0,0 +1,68 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-comment-list">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      .file {
+        border-top: 1px solid #ddd;
+        font-weight: bold;
+        margin: 10px 0 3px;
+        padding: 10px 0 5px;
+      }
+      .container {
+        display: flex;
+        margin: 5px 0;
+      }
+      .lineNum {
+        margin-right: .35em;
+        min-width: 7em;
+      }
+      .message {
+        flex: 1;
+        white-space: pre-wrap;
+        word-wrap: break-word;
+      }
+    </style>
+    <template is="dom-repeat" items="{{_files}}" as="file">
+      <div class="file">
+        <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
+      </div>
+      <template is="dom-repeat"
+                items="[[_computeCommentsForFile(file)]]" as="comment">
+        <div class="container">
+          <a class="lineNum"
+             href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+             <span hidden$="[[!comment.line]]">
+               <span>[[_computePatchDisplayName(comment)]]</span>
+               Line <span>[[comment.line]]</span>:
+             </span>
+             <span hidden$="[[comment.line]]">
+               File comment:
+             </span>
+          </a>
+          <div class="message">[[comment.message]]</div>
+        </div>
+      </template>
+    </template>
+  </template>
+  <script src="gr-comment-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
new file mode 100644
index 0000000..b40c18e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -0,0 +1,62 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-comment-list',
+
+    properties: {
+      changeNum: Number,
+      comments: {
+        type: Object,
+        observer: '_commentsChanged',
+      },
+      patchNum: Number,
+
+      _files: Array,
+    },
+
+    _commentsChanged: function(value) {
+      this._files = Object.keys(value || {}).sort();
+    },
+
+    _computeFileDiffURL: function(file, changeNum, patchNum) {
+      return '/c/' + changeNum + '/' + patchNum + '/' + file;
+    },
+
+    _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
+      var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
+      if (comment.line) {
+        // TODO(andybons): This is not correct if the comment is on the base.
+        diffURL += '#' + comment.line;
+      }
+      return diffURL;
+    },
+
+    _computeCommentsForFile: function(file) {
+      return this.comments[file];
+    },
+
+    _computePatchDisplayName: function(comment) {
+      if (comment.side == 'PARENT') {
+        return 'Base, ';
+      }
+      if (comment.patch_set != this.patchNum) {
+        return 'PS' + comment.patch_set + ', ';
+      }
+      return '';
+    }
+  });
+})();
diff --git a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
similarity index 66%
rename from polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 96c7188..3896ffa 100644
--- a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-confirm-dialog.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
 
 <dom-module id="gr-confirm-rebase-dialog">
   <template>
@@ -71,49 +71,5 @@
       </div>
     </gr-confirm-dialog>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-confirm-rebase-dialog',
-
-      /**
-       * Fired when the confirm button is pressed.
-       *
-       * @event confirm
-       */
-
-      /**
-       * Fired when the cancel button is pressed.
-       *
-       * @event cancel
-       */
-
-      properties: {
-        base: String,
-        clearParent: Boolean,
-      },
-
-      _handleConfirmTap: function(e) {
-        e.preventDefault();
-        this.fire('confirm', null, {bubbles: false});
-      },
-
-      _handleCancelTap: function(e) {
-        e.preventDefault();
-        this.fire('cancel', null, {bubbles: false});
-      },
-
-      _handleClearParentTap: function(e) {
-        var clear = Polymer.dom(e).rootTarget.checked;
-        if (clear) {
-          this.base = '';
-        }
-        this.$.parentInput.disabled = clear;
-        this.clearParent = clear;
-      },
-    });
-  })();
-  </script>
+  <script src="gr-confirm-rebase-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
new file mode 100644
index 0000000..42f2167
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -0,0 +1,56 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-rebase-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      base: String,
+      clearParent: Boolean,
+    },
+
+    _handleConfirmTap: function(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+
+    _handleClearParentTap: function(e) {
+      var clear = Polymer.dom(e).rootTarget.checked;
+      if (clear) {
+        this.base = '';
+      }
+      this.$.parentInput.disabled = clear;
+      this.clearParent = clear;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
similarity index 81%
rename from polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index 7db82e9..c02e11e 100644
--- a/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-rebase-dialog</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-confirm-rebase-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-rebase-dialog.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
new file mode 100644
index 0000000..77a262d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -0,0 +1,144 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+
+<dom-module id="gr-download-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+        padding: 1em;
+      }
+      ul {
+        list-style: none;
+        margin-bottom: .5em;
+      }
+      li {
+        display: inline-block;
+        margin: 0;
+        padding: 0;
+      }
+      li gr-button {
+        margin-right: 1em;
+      }
+      label,
+      input {
+        display: block;
+      }
+      label {
+        font-weight: bold;
+      }
+      input {
+        font-family: var(--monospace-font-family);
+        font-size: inherit;
+        margin-bottom: .5em;
+        width: 60em;
+      }
+      li[selected] gr-button {
+        color: #000;
+        font-weight: bold;
+        text-decoration: none;
+      }
+      header {
+        display: flex;
+        justify-content: space-between;
+      }
+      main {
+        border-bottom: 1px solid #ddd;
+        border-top: 1px solid #ddd;
+        padding: .5em;
+      }
+      footer {
+        display: flex;
+        justify-content: space-between;
+        padding-top: .75em;
+      }
+      .closeButtonContainer {
+        display: flex;
+        flex: 1;
+        justify-content: flex-end;
+      }
+      .patchFiles {
+        margin-right: 2em;
+      }
+      .patchFiles a,
+      .archives a {
+        display: inline-block;
+        margin-right: 1em;
+      }
+      .patchFiles a:last-of-type,
+      .archives a:last-of-type {
+        margin-right: 0;
+      }
+    </style>
+    <header>
+      <ul hidden$="[[!_schemes.length]]" hidden>
+        <template is="dom-repeat" items="[[_schemes]]" as="scheme">
+          <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]">
+            <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
+              [[scheme]]
+            </gr-button>
+          </li>
+        </template>
+      </ul>
+      <span class="closeButtonContainer">
+        <gr-button link on-tap="_handleCloseTap">Close</gr-button>
+      </span>
+    </header>
+    <main hidden$="[[!_schemes.length]]" hidden>
+      <template is="dom-repeat"
+          items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
+          as="command">
+        <div class="command">
+          <label>[[command.title]]</label>
+          <input is="iron-input"
+              type="text"
+              bind-value="[[command.command]]"
+              on-tap="_handleInputTap"
+              readonly>
+        </div>
+      </template>
+    </main>
+    <footer>
+      <div class="patchFiles">
+        <label>Patch file</label>
+        <div>
+          <a href$="[[_computeDownloadLink(change, patchNum)]]">
+            [[_computeDownloadFilename(change, patchNum)]]
+          </a>
+          <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
+            [[_computeZipDownloadFilename(change, patchNum)]]
+          </a>
+        </div>
+      </div>
+      <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
+        <label>Archive</label>
+        <div class="archives">
+          <template is="dom-repeat" items="[[config.archives]]" as="format">
+            <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
+              [[format]]
+            </a>
+          </template>
+        </div>
+      </div>
+    </footer>
+  </template>
+  <script src="gr-download-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
new file mode 100644
index 0000000..6677d62
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -0,0 +1,136 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-download-dialog',
+
+    /**
+     * Fired when the user presses the close button.
+     *
+     * @event close
+     */
+
+    properties: {
+      change: Object,
+      patchNum: String,
+      config: Object,
+
+      _schemes: {
+        type: Array,
+        value: function() { return []; },
+        computed: '_computeSchemes(change, patchNum)',
+        observer: '_schemesChanged',
+      },
+      _selectedScheme: String,
+    },
+
+    hostAttributes: {
+      role: 'dialog',
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    _computeDownloadCommands: function(change, patchNum, _selectedScheme) {
+      var commandObj;
+      for (var rev in change.revisions) {
+        if (change.revisions[rev]._number == patchNum) {
+          commandObj = change.revisions[rev].fetch[_selectedScheme].commands;
+          break;
+        }
+      }
+      var commands = [];
+      for (var title in commandObj) {
+        commands.push({
+          title: title,
+          command: commandObj[title],
+        });
+      }
+      return commands;
+    },
+
+    _computeZipDownloadLink: function(change, patchNum) {
+      return this._computeDownloadLink(change, patchNum, true);
+    },
+
+    _computeZipDownloadFilename: function(change, patchNum) {
+      return this._computeDownloadFilename(change, patchNum, true);
+    },
+
+    _computeDownloadLink: function(change, patchNum, zip) {
+      return this.changeBaseURL(change._number, patchNum) + '/patch?' +
+          (zip ? 'zip' : 'download');
+    },
+
+    _computeDownloadFilename: function(change, patchNum, zip) {
+      var shortRev;
+      for (var rev in change.revisions) {
+        if (change.revisions[rev]._number == patchNum) {
+          shortRev = rev.substr(0, 7);
+          break;
+        }
+      }
+      return shortRev + '.diff.' + (zip ? 'zip' : 'base64');
+    },
+
+    _computeArchiveDownloadLink: function(change, patchNum, format) {
+      return this.changeBaseURL(change._number, patchNum) +
+          '/archive?format=' + format;
+    },
+
+    _computeSchemes: function(change, patchNum) {
+      for (var rev in change.revisions) {
+        if (change.revisions[rev]._number == patchNum) {
+          var fetch = change.revisions[rev].fetch;
+          if (fetch) {
+            return Object.keys(fetch).sort();
+          }
+          break;
+        }
+      }
+      return [];
+    },
+
+    _computeSchemeSelected: function(scheme, selectedScheme) {
+      return scheme == selectedScheme;
+    },
+
+    _handleSchemeTap: function(e) {
+      e.preventDefault();
+      var el = Polymer.dom(e).rootTarget;
+      // TODO(andybons): Save as default scheme in preferences.
+      this._selectedScheme = el.getAttribute('data-scheme');
+    },
+
+    _handleInputTap: function(e) {
+      e.preventDefault();
+      Polymer.dom(e).rootTarget.select();
+    },
+
+    _handleCloseTap: function(e) {
+      e.preventDefault();
+      this.fire('close', null, {bubbles: false});
+    },
+
+    _schemesChanged: function(schemes) {
+      if (schemes.length == 0) { return; }
+      if (schemes.indexOf(this._selectedScheme) == -1) {
+        this._selectedScheme = schemes.sort()[0];
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-download-dialog-test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
similarity index 92%
rename from polygerrit-ui/app/test/gr-download-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 20ddf0a..2480c4a 100644
--- a/polygerrit-ui/app/test/gr-download-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-download-dialog</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-download-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-download-dialog.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
new file mode 100644
index 0000000..e010468
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -0,0 +1,159 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<dom-module id="gr-file-list">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      .row {
+        display: flex;
+        padding: .1em .25em;
+      }
+      .header {
+        font-weight: bold;
+      }
+      .positionIndicator,
+      .reviewed,
+      .status {
+        align-items: center;
+        display: inline-flex;
+      }
+      .reviewed,
+      .status {
+        justify-content: center;
+        width: 1.5em;
+      }
+      .positionIndicator {
+        justify-content: flex-start;
+        visibility: hidden;
+        width: 1.25em;
+      }
+      .row[selected] {
+        background-color: #ebf5fb;
+      }
+      .row[selected] .positionIndicator {
+        visibility: visible;
+      }
+      .path {
+        flex: 1;
+        overflow: hidden;
+        padding-left: .35em;
+        text-decoration: none;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      .row:not(.header) .path:hover {
+        text-decoration: underline;
+      }
+      .comments,
+      .stats {
+        text-align: right;
+      }
+      .comments {
+        min-width: 10em;
+      }
+      .stats {
+        min-width: 7em;
+      }
+      .invisible {
+        visibility: hidden;
+      }
+      .row:not(.header) .stats {
+        font-family: var(--monospace-font-family);
+      }
+      .added {
+        color: #388E3C;
+      }
+      .removed {
+        color: #D32F2F;
+      }
+      .reviewed input[type="checkbox"] {
+        display: inline-block;
+      }
+      .drafts {
+        color: #C62828;
+        font-weight: bold;
+      }
+      @media screen and (max-width: 50em) {
+        .row[selected] {
+          background-color: transparent;
+        }
+        .positionIndicator,
+        .stats {
+          display: none;
+        }
+        .reviewed,
+        .status {
+          justify-content: flex-start;
+        }
+        .comments {
+          min-width: initial;
+        }
+      }
+    </style>
+    <gr-ajax id="filesXHR"
+        url="[[_computeFilesURL(changeNum, patchNum)]]"
+        on-response="_handleResponse"></gr-ajax>
+    <gr-ajax id="draftsXHR"
+        url="[[_computeDraftsURL(changeNum, patchNum)]]"
+        last-response="{{_drafts}}"></gr-ajax>
+    <gr-ajax id="reviewedXHR"
+        url="[[_computeReviewedURL(changeNum, patchNum)]]"
+        last-response="{{_reviewed}}"></gr-ajax>
+    </gr-ajax>
+
+    <div class="row header">
+      <div class="positionIndicator"></div>
+      <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
+      <div class="status"></div>
+      <div class="path">Path</div>
+      <div class="comments">Comments</div>
+      <div class="stats">Stats</div>
+    </div>
+    <template is="dom-repeat" items="{{files}}" as="file">
+      <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
+        <div class="positionIndicator">&#x25b6;</div>
+        <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
+          <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
+              data-path$="[[file.__path]]" on-change="_handleReviewedChange">
+        </div>
+        <div class$="[[_computeClass('status', file.__path)]]">
+          [[_computeFileStatus(file.status)]]
+        </div>
+        <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
+          [[_computeFileDisplayName(file.__path)]]
+        </a>
+        <div class="comments">
+          <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
+          [[_computeCommentsString(comments, patchNum, file.__path)]]
+        </div>
+        <div class$="[[_computeClass('stats', file.__path)]]">
+          <span class="added">+[[file.lines_inserted]]</span>
+          <span class="removed">-[[file.lines_deleted]]</span>
+        </div>
+      </div>
+    </template>
+  </template>
+  <script src="gr-file-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
new file mode 100644
index 0000000..9fe5ca1
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -0,0 +1,205 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+
+  Polymer({
+    is: 'gr-file-list',
+
+    properties: {
+      patchNum: String,
+      changeNum: String,
+      comments: Object,
+      files: Array,
+      selectedIndex: {
+        type: Number,
+        notify: true,
+      },
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+
+      _loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+      _drafts: Object,
+      _reviewed: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _xhrPromise: Object,  // Used for testing.
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+      Gerrit.RESTClientBehavior,
+    ],
+
+    reload: function() {
+      if (!this.changeNum || !this.patchNum) {
+        return Promise.resolve();
+      }
+      return Promise.all([
+        this.$.filesXHR.generateRequest().completes,
+        app.accountReady.then(function() {
+          this._loggedIn = app.loggedIn;
+          if (!app.loggedIn) { return; }
+          this.$.draftsXHR.generateRequest();
+          this.$.reviewedXHR.generateRequest();
+        }.bind(this)),
+      ]);
+    },
+
+    _computeFilesURL: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/files';
+    },
+
+    _computeCommentsString: function(comments, patchNum, path) {
+      var patchComments = (comments[path] || []).filter(function(c) {
+        return c.patch_set == patchNum;
+      });
+      var num = patchComments.length;
+      if (num == 0) { return ''; }
+      if (num == 1) { return '1 comment'; }
+      if (num > 1) { return num + ' comments'; }
+    },
+
+    _computeReviewedURL: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
+    },
+
+    _computeReviewed: function(file, _reviewed) {
+      return _reviewed.indexOf(file.__path) != -1;
+    },
+
+    _handleReviewedChange: function(e) {
+      var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
+      var index = this._reviewed.indexOf(path);
+      var reviewed = index != -1;
+      if (reviewed) {
+        this.splice('_reviewed', index, 1);
+      } else {
+        this.push('_reviewed', path);
+      }
+
+      var method = reviewed ? 'DELETE' : 'PUT';
+      var url = this.changeBaseURL(this.changeNum, this.patchNum) +
+          '/files/' + encodeURIComponent(path) + '/reviewed';
+      this._send(method, url).catch(function(err) {
+        alert('Couldn’t change file review status. Check the console ' +
+            'and contact the PolyGerrit team for assistance.');
+        throw err;
+      }.bind(this));
+    },
+
+    _computeDraftsURL: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/drafts';
+    },
+
+    _computeDraftsString: function(drafts, path) {
+      var num = (drafts[path] || []).length;
+      if (num == 0) { return ''; }
+      if (num == 1) { return '1 draft'; }
+      if (num > 1) { return num + ' drafts'; }
+    },
+
+    _handleResponse: function(e, req) {
+      var result = e.detail.response;
+      var paths = Object.keys(result).sort();
+      var files = [];
+      for (var i = 0; i < paths.length; i++) {
+        var info = result[paths[i]];
+        info.__path = paths[i];
+        info.lines_inserted = info.lines_inserted || 0;
+        info.lines_deleted = info.lines_deleted || 0;
+        files.push(info);
+      }
+      this.files = files;
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+
+      switch (e.keyCode) {
+        case 74:  // 'j'
+          e.preventDefault();
+          this.selectedIndex =
+              Math.min(this.files.length - 1, this.selectedIndex + 1);
+          break;
+        case 75:  // 'k'
+          e.preventDefault();
+          this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+          break;
+        case 219:  // '['
+          e.preventDefault();
+          this._openSelectedFile(this.files.length - 1);
+          break;
+        case 221:  // ']'
+          e.preventDefault();
+          this._openSelectedFile(0);
+          break;
+        case 13:  // <enter>
+        case 79:  // 'o'
+          e.preventDefault();
+          this._openSelectedFile();
+          break;
+      }
+    },
+
+    _openSelectedFile: function(opt_index) {
+      if (opt_index != null) {
+        this.selectedIndex = opt_index;
+      }
+      page.show(this._computeDiffURL(this.changeNum, this.patchNum,
+          this.files[this.selectedIndex].__path));
+    },
+
+    _computeFileSelected: function(index, selectedIndex) {
+      return index == selectedIndex;
+    },
+
+    _computeFileStatus: function(status) {
+      return status || 'M';
+    },
+
+    _computeDiffURL: function(changeNum, patchNum, path) {
+      return '/c/' + changeNum + '/' + patchNum + '/' + path;
+    },
+
+    _computeFileDisplayName: function(path) {
+      return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+    },
+
+    _computeClass: function(baseClass, path) {
+      var classes = [baseClass];
+      if (path == COMMIT_MESSAGE_PATH) {
+        classes.push('invisible');
+      }
+      return classes.join(' ');
+    },
+
+    _send: function(method, url) {
+      var xhr = document.createElement('gr-request');
+      this._xhrPromise = xhr.send({
+        method: method,
+        url: url,
+      });
+      return this._xhrPromise;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-file-list-test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-file-list-test.html
rename to polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index aabca50..06a01c6 100644
--- a/polygerrit-ui/app/test/gr-file-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -18,14 +18,14 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-file-list</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-file-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-file-list.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
new file mode 100644
index 0000000..5733acd
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -0,0 +1,125 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+
+<link rel="import" href="../gr-comment-list/gr-comment-list.html">
+
+<dom-module id="gr-message">
+  <template>
+    <style>
+      :host {
+        border-top: 1px solid #ddd;
+        display: block;
+        position: relative;
+      }
+      :host(:not([expanded])) {
+        cursor: pointer;
+      }
+      gr-avatar {
+        position: absolute;
+        left: var(--default-horizontal-margin);
+      }
+      .collapsed .contentContainer {
+        color: #777;
+        white-space: nowrap;
+        overflow-x: hidden;
+        text-overflow: ellipsis;
+      }
+      .showAvatar.expanded .contentContainer {
+        margin-left: calc(var(--default-horizontal-margin) + 2.5em);
+        padding: 10px 0;
+      }
+      .showAvatar.collapsed .contentContainer {
+        margin-left: calc(var(--default-horizontal-margin) + 1.75em);
+        padding: 10px 75px 10px 0;
+      }
+      .hideAvatar.collapsed .contentContainer,
+      .hideAvatar.expanded .contentContainer {
+        margin-left: 0;
+        padding: 10px 75px 10px 0;
+      }
+      .collapsed gr-avatar {
+        top: 8px;
+        height: 1.75em;
+        width: 1.75em;
+      }
+      .expanded gr-avatar {
+        top: 12px;
+        height: 2.5em;
+        width: 2.5em;
+      }
+      .name {
+        font-weight: bold;
+      }
+      .content {
+        font-family: var(--monospace-font-family);
+      }
+      .collapsed .name,
+      .collapsed .content,
+      .collapsed .message {
+        display: inline;
+      }
+      .collapsed gr-comment-list,
+      .collapsed .replyContainer {
+        display: none;
+      }
+      .collapsed .name {
+        color: var(--default-text-color);
+      }
+      .expanded .name {
+        cursor: pointer;
+      }
+      .date {
+        color: #666;
+        position: absolute;
+        right: var(--default-horizontal-margin);
+        top: 10px;
+      }
+      .replyContainer {
+        padding: .5em 0 1em;
+      }
+    </style>
+    <div class$="[[_computeClass(expanded, showAvatar)]]">
+      <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
+      <div class="contentContainer">
+        <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div>
+        <div class="content">
+          <gr-linked-text class="message"
+              pre="[[expanded]]"
+              content="[[message.message]]"
+              disabled="[[!expanded]]"
+              config="[[projectConfig.commentlinks]]"></gr-linked-text>
+          <gr-comment-list
+              comments="[[comments]]"
+              change-num="[[changeNum]]"
+              patch-num="[[message._revision_number]]"></gr-comment-list>
+        </div>
+        <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
+          <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
+        </a>
+      </div>
+      <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
+        <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
+      </div>
+    </div>
+  </template>
+  <script src="gr-message.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
new file mode 100644
index 0000000..1ab5e6c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -0,0 +1,111 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-message',
+
+    /**
+     * Fired when this message's permalink is tapped.
+     *
+     * @event scroll-to
+     */
+
+    /**
+     * Fired when this message's reply link is tapped.
+     *
+     * @event reply
+     */
+
+    listeners: {
+      'tap': '_handleTap',
+    },
+
+    properties: {
+      changeNum: Number,
+      message: Object,
+      comments: {
+        type: Object,
+        observer: '_commentsChanged',
+      },
+      expanded: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+      showAvatar: {
+        type: Boolean,
+        value: false,
+      },
+      showReplyButton: {
+        type: Boolean,
+        value: false,
+      },
+      projectConfig: Object,
+    },
+
+    ready: function() {
+      app.configReady.then(function(cfg) {
+        this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
+            this.message && this.message.author;
+      }.bind(this));
+    },
+
+    _commentsChanged: function(value) {
+      this.expanded = Object.keys(value || {}).length > 0;
+    },
+
+    _handleTap: function(e) {
+      if (this.expanded) { return; }
+      this.expanded = true;
+    },
+
+    _handleNameTap: function(e) {
+      if (!this.expanded) { return; }
+      e.stopPropagation();
+      this.expanded = false;
+    },
+
+    _computeClass: function(expanded, showAvatar) {
+      var classes = [];
+      classes.push(expanded ? 'expanded' : 'collapsed');
+      classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
+      return classes.join(' ');
+    },
+
+    _computeMessageHash: function(message) {
+      return '#message-' + message.id;
+    },
+
+    _handleLinkTap: function(e) {
+      e.preventDefault();
+
+      this.fire('scroll-to', {message: this.message}, {bubbles: false});
+
+      var hash = this._computeMessageHash(this.message);
+      // Don't add the hash to the window history if it's already there.
+      // Otherwise you mess up expected back button behavior.
+      if (window.location.hash == hash) { return; }
+      // Change the URL but don’t trigger a nav event. Otherwise it will
+      // reload the page.
+      page.show(window.location.pathname + hash, null, false);
+    },
+
+    _handleReplyTap: function(e) {
+      e.preventDefault();
+      this.fire('reply', {message: this.message});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-message-test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
similarity index 78%
rename from polygerrit-ui/app/test/gr-message-test.html
rename to polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 503da1b..0f09b70 100644
--- a/polygerrit-ui/app/test/gr-message-test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-message</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-message.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-message.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
new file mode 100644
index 0000000..8a66d03
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -0,0 +1,62 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../gr-message/gr-message.html">
+
+<dom-module id="gr-messages-list">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      .header {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: .35em;
+      }
+      .header,
+      gr-message {
+        padding: 0 var(--default-horizontal-margin);
+      }
+      .highlighted {
+        animation: 3s fadeOut;
+      }
+      @keyframes fadeOut {
+        0% { background-color: #fff9c4; }
+        100% { background-color: #fff; }
+      }
+    </style>
+    <div class="header">
+      <h3>Messages</h3>
+      <gr-button link on-tap="_handleExpandCollapseTap">
+        [[_computeExpandCollapseMessage(_expanded)]]
+      </gr-button>
+    </div>
+    <template is="dom-repeat" items="[[messages]]" as="message">
+      <gr-message
+          change-num="[[changeNum]]"
+          message="[[message]]"
+          comments="[[_computeCommentsForMessage(comments, message, index)]]"
+          project-config="[[projectConfig]]"
+          show-reply-button="[[showReplyButtons]]"
+          on-scroll-to="_handleScrollTo"
+          data-message-id$="[[message.id]]"></gr-message>
+    </template>
+  </template>
+  <script src="gr-messages-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
new file mode 100644
index 0000000..1b9ce14
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -0,0 +1,111 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-messages-list',
+
+    properties: {
+      changeNum: Number,
+      messages: {
+        type: Array,
+        value: function() { return []; },
+      },
+      comments: Object,
+      projectConfig: Object,
+      topMargin: Number,
+      showReplyButtons: {
+        type: Boolean,
+        value: false,
+      },
+
+      _expanded: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    scrollToMessage: function(messageID) {
+      var el = this.$$('[data-message-id="' + messageID + '"]');
+      if (!el) { return; }
+
+      el.expanded = true;
+      var top = el.offsetTop;
+      for (var offsetParent = el.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+      window.scrollTo(0, top - this.topMargin);
+      this._highlightEl(el);
+    },
+
+    _highlightEl: function(el) {
+      var highlightedEls =
+          Polymer.dom(this.root).querySelectorAll('.highlighted');
+      for (var i = 0; i < highlightedEls.length; i++) {
+        highlightedEls[i].classList.remove('highlighted');
+      }
+      function handleAnimationEnd() {
+        el.removeEventListener('animationend', handleAnimationEnd);
+        el.classList.remove('highlighted');
+      }
+      el.addEventListener('animationend', handleAnimationEnd);
+      el.classList.add('highlighted');
+    },
+
+    _handleExpandCollapseTap: function(e) {
+      e.preventDefault();
+      this._expanded = !this._expanded;
+      var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message');
+      for (var i = 0; i < messageEls.length; i++) {
+        messageEls[i].expanded = this._expanded;
+      }
+    },
+
+    _handleScrollTo: function(e) {
+      this.scrollToMessage(e.detail.message.id);
+    },
+
+    _computeExpandCollapseMessage: function(expanded) {
+      return expanded ? 'Collapse all' : 'Expand all';
+    },
+
+    _computeCommentsForMessage: function(comments, message, index) {
+      comments = comments || {};
+      var messages = this.messages || [];
+      var msgComments = {};
+      var mDate = util.parseDate(message.date);
+      var nextMDate;
+      if (index < messages.length - 1) {
+        nextMDate = util.parseDate(messages[index + 1].date);
+      }
+      for (var file in comments) {
+        var fileComments = comments[file];
+        for (var i = 0; i < fileComments.length; i++) {
+          var cDate = util.parseDate(fileComments[i].updated);
+          if (cDate >= mDate) {
+            if (nextMDate && cDate >= nextMDate) {
+              continue;
+            }
+            msgComments[file] = msgComments[file] || [];
+            msgComments[file].push(fileComments[i]);
+          }
+        }
+      }
+      return msgComments;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-messages-list-test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
similarity index 89%
rename from polygerrit-ui/app/test/gr-messages-list-test.html
rename to polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index a8ead1d..5a562ba 100644
--- a/polygerrit-ui/app/test/gr-messages-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-messages-list</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-messages-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-messages-list.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
new file mode 100644
index 0000000..e93d008
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -0,0 +1,132 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+
+<dom-module id="gr-related-changes-list">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      h3 {
+        margin: .5em 0 0;
+      }
+      section {
+        margin-bottom: 1em;
+      }
+      a {
+        display: block;
+      }
+      .relatedChanges a {
+        display: inline-block;
+      }
+      .strikethrough {
+        color: #666;
+        text-decoration: line-through;
+      }
+      .status {
+        color: #666;
+        font-weight: bold;
+      }
+      .notCurrent {
+        color: #e65100;
+      }
+      .indirectAncestor {
+        color: #33691e;
+      }
+      .submittable {
+        color: #1b5e20;
+      }
+      .hidden {
+        display: none;
+      }
+    </style>
+    <gr-ajax id="relatedXHR"
+        url="[[_computeRelatedURL(change._number, patchNum)]]"
+        last-response="{{_relatedResponse}}"></gr-ajax>
+    <gr-ajax id="submittedTogetherXHR"
+        url="[[_computeSubmittedTogetherURL(change._number)]]"
+        last-response="{{_submittedTogether}}"></gr-ajax>
+    <gr-ajax id="conflictsXHR"
+        url="/changes/"
+        params="[[_computeConflictsQueryParams(change._number)]]"
+        last-response="{{_conflicts}}"></gr-ajax>
+    <gr-ajax id="cherryPicksXHR"
+        url="/changes/"
+        params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]"
+        last-response="{{_cherryPicks}}"></gr-ajax>
+    <gr-ajax id="sameTopicXHR"
+        url="/changes/"
+        params="[[_computeSameTopicQueryParams(change.topic)]]"
+        last-response="{{_sameTopic}}"></gr-ajax>
+
+    <div hidden$="[[!_loading]]">Loading...</div>
+    <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
+      <h4>Relation Chain</h4>
+      <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change">
+        <div>
+          <a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]"
+              class$="[[_computeLinkClass(change)]]">
+            [[change.commit.subject]]
+          </a>
+          <span class$="[[_computeChangeStatusClass(change)]]">
+            ([[_computeChangeStatus(change)]])
+          </span>
+        </div>
+      </template>
+    </section>
+    <section hidden$="[[!_submittedTogether.length]]" hidden>
+      <h4>Submitted together</h4>
+      <template is="dom-repeat" items="[[_submittedTogether]]" as="change">
+        <a href$="[[_computeChangeURL(change._number)]]"
+            class$="[[_computeLinkClass(change)]]">
+          [[change.project]]: [[change.branch]]: [[change.subject]]
+        </a>
+      </template>
+    </section>
+    <section hidden$="[[!_sameTopic.length]]" hidden>
+      <h4>Same topic</h4>
+      <template is="dom-repeat" items="[[_sameTopic]]" as="change">
+        <a href$="[[_computeChangeURL(change._number)]]"
+            class$="[[_computeLinkClass(change)]]">
+          [[change.project]]: [[change.branch]]: [[change.subject]]
+        </a>
+      </template>
+    </section>
+    <section hidden$="[[!_conflicts.length]]" hidden>
+      <h4>Merge conflicts</h4>
+      <template is="dom-repeat" items="[[_conflicts]]" as="change">
+        <a href$="[[_computeChangeURL(change._number)]]"
+            class$="[[_computeLinkClass(change)]]">
+          [[change.subject]]
+        </a>
+      </template>
+    </section>
+    <section hidden$="[[!_cherryPicks.length]]" hidden>
+      <h4>Cherry picks</h4>
+      <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
+        <a href$="[[_computeChangeURL(change._number)]]"
+            class$="[[_computeLinkClass(change)]]">
+          [[change.subject]]
+        </a>
+      </template>
+    </section>
+  </template>
+  <script src="gr-related-changes-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
new file mode 100644
index 0000000..f3a298e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -0,0 +1,242 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-related-changes-list',
+
+    properties: {
+      change: Object,
+      patchNum: String,
+      serverConfig: {
+        type: Object,
+        observer: '_serverConfigChanged',
+      },
+      hidden: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+
+      _loading: Boolean,
+      _resolveServerConfigReady: Function,
+      _serverConfigReady: {
+        type: Object,
+        value: function() {
+          return new Promise(function(resolve) {
+            this._resolveServerConfigReady = resolve;
+          }.bind(this));
+        }
+      },
+      _connectedRevisions: {
+        type: Array,
+        computed: '_computeConnectedRevisions(change, patchNum, ' +
+            '_relatedResponse.changes)',
+      },
+      _relatedResponse: Object,
+      _submittedTogether: Array,
+      _conflicts: Array,
+      _cherryPicks: Array,
+      _sameTopic: Array,
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    observers: [
+      '_resultsChanged(_relatedResponse.changes, _submittedTogether, ' +
+          '_conflicts, _cherryPicks, _sameTopic)',
+    ],
+
+    reload: function() {
+      if (!this.change || !this.patchNum) {
+        return Promise.resolve();
+      }
+      this._loading = true;
+      var promises = [
+        this.$.relatedXHR.generateRequest().completes,
+        this.$.submittedTogetherXHR.generateRequest().completes,
+        this.$.conflictsXHR.generateRequest().completes,
+        this.$.cherryPicksXHR.generateRequest().completes,
+      ];
+
+      return this._serverConfigReady.then(function() {
+        if (this.change.topic &&
+            !this.serverConfig.change.submit_whole_topic) {
+          return this.$.sameTopicXHR.generateRequest().completes;
+        } else {
+          this._sameTopic = [];
+        }
+        return Promise.resolve();
+      }.bind(this)).then(Promise.all(promises)).then(function() {
+        this._loading = false;
+      }.bind(this));
+    },
+
+    _computeRelatedURL: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/related';
+    },
+
+    _computeSubmittedTogetherURL: function(changeNum) {
+      return this.changeBaseURL(changeNum) + '/submitted_together';
+    },
+
+    _computeConflictsQueryParams: function(changeNum) {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.CURRENT_REVISION,
+          this.ListChangesOption.CURRENT_COMMIT
+      );
+      return {
+        O: options,
+        q: 'status:open is:mergeable conflicts:' + changeNum,
+      };
+    },
+
+    _computeCherryPicksQueryParams: function(project, changeID, changeNum) {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.CURRENT_REVISION,
+          this.ListChangesOption.CURRENT_COMMIT
+      );
+      var query = [
+        'project:' + project,
+        'change:' + changeID,
+        '-change:' + changeNum,
+        '-is:abandoned',
+      ].join(' ');
+      return {
+        O: options,
+        q: query
+      }
+    },
+
+    _computeSameTopicQueryParams: function(topic) {
+      var options = this.listChangesOptionsToHex(
+          this.ListChangesOption.LABELS,
+          this.ListChangesOption.CURRENT_REVISION,
+          this.ListChangesOption.CURRENT_COMMIT,
+          this.ListChangesOption.DETAILED_LABELS
+      );
+      return {
+        O: options,
+        q: 'status:open topic:' + topic,
+      };
+    },
+
+    _computeChangeURL: function(changeNum, patchNum) {
+      var urlStr = '/c/' + changeNum;
+      if (patchNum != null) {
+        urlStr += '/' + patchNum;
+      }
+      return urlStr;
+    },
+
+    _computeLinkClass: function(change) {
+      if (change.status == this.ChangeStatus.ABANDONED) {
+        return 'strikethrough';
+      }
+    },
+
+    _computeChangeStatusClass: function(change) {
+      var classes = ['status'];
+      if (change._revision_number != change._current_revision_number) {
+        classes.push('notCurrent');
+      } else if (this._isIndirectAncestor(change)) {
+        classes.push('indirectAncestor');
+      } else if (change.submittable) {
+        classes.push('submittable');
+      } else if (change.status == this.ChangeStatus.NEW) {
+        classes.push('hidden');
+      }
+      return classes.join(' ');
+    },
+
+    _computeChangeStatus: function(change) {
+      switch (change.status) {
+        case this.ChangeStatus.MERGED:
+          return 'Merged';
+        case this.ChangeStatus.ABANDONED:
+          return 'Abandoned';
+        case this.ChangeStatus.DRAFT:
+          return 'Draft';
+      }
+      if (change._revision_number != change._current_revision_number) {
+        return 'Not current';
+      } else if (this._isIndirectAncestor(change)) {
+        return 'Indirect ancestor';
+      } else if (change.submittable) {
+        return 'Submittable';
+      }
+      return ''
+    },
+
+    _serverConfigChanged: function(config) {
+      this._resolveServerConfigReady(config);
+    },
+
+    _resultsChanged: function(related, submittedTogether, conflicts,
+        cherryPicks, sameTopic) {
+      var results = [
+        related,
+        submittedTogether,
+        conflicts,
+        cherryPicks,
+        sameTopic
+      ];
+      for (var i = 0; i < results.length; i++) {
+        if (results[i].length > 0) {
+          this.hidden = false;
+          return;
+        }
+      }
+      this.hidden = true;
+    },
+
+    _isIndirectAncestor: function(change) {
+      return this._connectedRevisions.indexOf(change.commit.commit) == -1;
+    },
+
+    _computeConnectedRevisions: function(change, patchNum, relatedChanges) {
+      var connected = [];
+      var changeRevision;
+      for (var rev in change.revisions) {
+        if (change.revisions[rev]._number == patchNum) {
+          changeRevision = rev;
+        }
+      }
+      var commits = relatedChanges.map(function(c) { return c.commit; });
+      var pos = commits.length - 1;
+
+      while (pos >= 0) {
+        var commit = commits[pos].commit;
+        connected.push(commit);
+        if (commit == changeRevision) {
+          break;
+        }
+        pos--;
+      }
+      while (pos >= 0) {
+        for (var i = 0; i < commits[pos].parents.length; i++) {
+          if (connected.indexOf(commits[pos].parents[i].commit) != -1) {
+            connected.push(commits[pos].commit);
+            break;
+          }
+        }
+        --pos;
+      }
+      return connected;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-related-changes-list-test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-related-changes-list-test.html
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index d6fc3c0..7e0c236 100644
--- a/polygerrit-ui/app/test/gr-related-changes-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-related-changes-list</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-related-changes-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-related-changes-list.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
new file mode 100644
index 0000000..ab21e6c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -0,0 +1,148 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<dom-module id="gr-reply-dialog">
+  <style>
+    :host {
+      display: block;
+      max-height: 90vh;
+    }
+    :host([disabled]) {
+      pointer-events: none;
+    }
+    :host([disabled]) .container {
+      opacity: .5;
+    }
+    .container {
+      display: flex;
+      flex-direction: column;
+      max-height: 90vh;
+    }
+    section {
+      border-top: 1px solid #ddd;
+      padding: .5em .75em;
+    }
+    .textareaContainer,
+    .labelsContainer,
+    .actionsContainer {
+      flex-shrink: 0;
+    }
+    .textareaContainer {
+      position: relative;
+    }
+    iron-autogrow-textarea {
+      padding: 0;
+      font-family: var(--monospace-font-family);
+    }
+    .message {
+      border: none;
+      width: 100%;
+    }
+    .labelContainer:not(:first-of-type) {
+      margin-top: .5em;
+    }
+    .labelName {
+      display: inline-block;
+      width: 7em;
+      margin-right: .5em;
+      white-space: nowrap;
+    }
+    iron-selector {
+      display: inline-flex;
+    }
+    iron-selector > gr-button {
+      margin-right: .25em;
+    }
+    iron-selector > gr-button:first-of-type {
+      border-top-left-radius: 2px;
+      border-bottom-left-radius: 2px;
+    }
+    iron-selector > gr-button:last-of-type {
+      border-top-right-radius: 2px;
+      border-bottom-right-radius: 2px;
+    }
+    iron-selector > gr-button.iron-selected {
+      background-color: #ddd;
+    }
+    .draftsContainer {
+      overflow-y: auto;
+    }
+    .draftsContainer h3 {
+      margin-top: .25em;
+    }
+    .actionsContainer {
+      display: flex;
+      justify-content: space-between;
+    }
+    .action:link,
+    .action:visited {
+      color: #00e;
+    }
+  </style>
+  <template>
+    <gr-ajax id="draftsXHR"
+        url="[[_computeDraftsURL(changeNum)]]"
+        last-response="{{_drafts}}"></gr-ajax>
+    <div class="container">
+      <section class="textareaContainer">
+        <iron-autogrow-textarea
+            id="textarea"
+            class="message"
+            placeholder="Say something..."
+            disabled="{{disabled}}"
+            rows="4"
+            max-rows="15"
+            bind-value="{{draft}}"></iron-autogrow-textarea>
+      </section>
+      <section class="labelsContainer">
+        <template is="dom-repeat"
+            items="[[_computeLabelArray(permittedLabels)]]" as="label">
+          <div class="labelContainer">
+            <span class="labelName">[[label]]</span>
+            <iron-selector data-label$="[[label]]"
+                selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
+              <template is="dom-repeat"
+                  items="[[_computePermittedLabelValues(permittedLabels, label)]]"
+                  as="value">
+                <gr-button data-value$="[[value]]">[[value]]</gr-button>
+              </template>
+            </iron-selector>
+          </div>
+        </template>
+      </section>
+      <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
+        <h3>[[_computeDraftsTitle(_drafts)]]</h3>
+        <gr-comment-list
+            comments="[[_drafts]]"
+            change-num="[[changeNum]]"
+            patch-num="[[patchNum]]"></gr-comment-list>
+      </section>
+      <section class="actionsContainer">
+        <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
+        <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
+      </section>
+    </div>
+  </template>
+  <script src="gr-reply-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
new file mode 100644
index 0000000..3cd6e12
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -0,0 +1,171 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-reply-dialog',
+
+    /**
+     * Fired when a reply is successfully sent.
+     *
+     * @event send
+     */
+
+    /**
+     * Fired when the user presses the cancel button.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      changeNum: String,
+      patchNum: String,
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      draft: {
+        type: String,
+        value: '',
+      },
+      labels: Object,
+      permittedLabels: Object,
+
+      _account: Object,
+      _drafts: Object,
+      _xhrPromise: Object,  // Used for testing.
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    ready: function() {
+      app.accountReady.then(function() {
+        this._account = app.account;
+      }.bind(this));
+    },
+
+    reload: function() {
+      return this.$.draftsXHR.generateRequest().completes;
+    },
+
+    focus: function() {
+      this.async(function() {
+        this.$.textarea.textarea.focus();
+      }.bind(this));
+    },
+
+    _computeDraftsURL: function(changeNum) {
+      return '/changes/' + changeNum + '/drafts';
+    },
+
+    _computeHideDraftList: function(drafts) {
+      return Object.keys(drafts || {}).length == 0;
+    },
+
+    _computeDraftsTitle: function(drafts) {
+      var total = 0;
+      for (var file in drafts) {
+        total += drafts[file].length;
+      }
+      if (total == 0) { return ''; }
+      if (total == 1) { return '1 Draft'; }
+      if (total > 1) { return total + ' Drafts'; }
+    },
+
+    _computeLabelArray: function(labelsObj) {
+      return Object.keys(labelsObj).sort();
+    },
+
+    _computeIndexOfLabelValue: function(
+        labels, permittedLabels, labelName, account) {
+      var t = labels[labelName];
+      if (!t) { return null; }
+      var labelValue = t.default_value;
+
+      // Is there an existing vote for the current user? If so, use that.
+      var votes = labels[labelName];
+      if (votes.all && votes.all.length > 0) {
+        for (var i = 0; i < votes.all.length; i++) {
+          if (votes.all[i]._account_id == account._account_id) {
+            labelValue = votes.all[i].value;
+            break;
+          }
+        }
+      }
+
+      var len = permittedLabels[labelName] != null ?
+          permittedLabels[labelName].length : 0;
+      for (var i = 0; i < len; i++) {
+        var val = parseInt(permittedLabels[labelName][i], 10);
+        if (val == labelValue) {
+          return i;
+        }
+      }
+      return null;
+    },
+
+    _computePermittedLabelValues: function(permittedLabels, label) {
+      return permittedLabels[label];
+    },
+
+    _cancelTapHandler: function(e) {
+      e.preventDefault();
+      this._drafts = null;
+      this.fire('cancel', null, {bubbles: false});
+    },
+
+    _sendTapHandler: function(e) {
+      e.preventDefault();
+      var obj = {
+        drafts: 'PUBLISH_ALL_REVISIONS',
+        labels: {},
+      };
+      for (var label in this.permittedLabels) {
+        var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+        var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
+        selectedVal = parseInt(selectedVal, 10);
+        obj.labels[label] = selectedVal;
+      }
+      if (this.draft != null) {
+        obj.message = this.draft;
+      }
+      this.disabled = true;
+      this._send(obj).then(function(req) {
+        this.fire('send', null, {bubbles: false});
+        this.draft = '';
+        this.disabled = false;
+        this._drafts = null;
+      }.bind(this)).catch(function(err) {
+        alert('Oops. Something went wrong. Check the console and bug the ' +
+            'PolyGerrit team for assistance.');
+        throw err;
+      }.bind(this));
+    },
+
+    _send: function(payload) {
+      var xhr = document.createElement('gr-request');
+      this._xhrPromise = xhr.send({
+        method: 'POST',
+        url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
+        body: payload,
+      });
+
+      return this._xhrPromise;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-reply-dialog-test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
similarity index 90%
rename from polygerrit-ui/app/test/gr-reply-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 60e2ab0..a6f4671 100644
--- a/polygerrit-ui/app/test/gr-reply-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reply-dialog</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-reply-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-reply-dialog.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
new file mode 100644
index 0000000..d20fd01
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -0,0 +1,118 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<dom-module id="gr-reviewer-list">
+  <style>
+    :host {
+      display: block;
+    }
+    :host([disabled]) {
+      opacity: .8;
+      pointer-events: none;
+    }
+    .autocompleteContainer {
+      position: relative;
+    }
+    .inputContainer {
+      display: flex;
+      margin-top: .25em;
+    }
+    .inputContainer input {
+      flex: 1;
+      font: inherit;
+    }
+    .dropdown {
+      background-color: #fff;
+      box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+      position: absolute;
+      left: 0;
+      top: 100%;
+    }
+    .dropdown .reviewer {
+      cursor: pointer;
+      padding: .5em .75em;
+    }
+    .dropdown .reviewer[selected] {
+      background-color: #ccc;
+    }
+    .remove,
+    .cancel {
+      color: #999;
+    }
+    .remove {
+      font-size: .9em;
+    }
+    .cancel {
+      font-size: 2em;
+      line-height: 1;
+      padding: 0 .15em;
+      text-decoration: none;
+    }
+  </style>
+  <template>
+    <gr-ajax id="autocompleteXHR"
+        url="[[_computeAutocompleteURL(change)]]"
+        params="[[_computeAutocompleteParams(_inputVal)]]"
+        on-response="_handleResponse"></gr-ajax>
+
+    <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
+      <div class="reviewer">
+        <gr-account-link account="[[reviewer]]" show-email></gr-account-link>
+        <gr-button link
+            class="remove"
+            data-account-id$="[[reviewer._account_id]]"
+            on-tap="_handleRemoveTap"
+            hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
+      </div>
+    </template>
+    <div class="controlsContainer" hidden$="[[!mutable]]">
+      <div class="autocompleteContainer" hidden$="[[!_showInput]]">
+        <div class="inputContainer">
+          <input is="iron-input" id="input"
+              bind-value="{{_inputVal}}" disabled$="[[disabled]]">
+          <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
+        </div>
+        <div class="dropdown" hidden$="[[_hideAutocomplete]]">
+          <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
+            <div class="reviewer"
+                data-index$="[[index]]"
+                on-mouseenter="_handleMouseEnterItem"
+                on-tap="_handleItemTap"
+                selected$="[[_computeSelected(index, _selectedIndex)]]">
+              <template is="dom-if" if="[[reviewer.account]]">
+                <gr-account-label
+                    account="[[reviewer.account]]" show-email></gr-account-label>
+              </template>
+              <template is="dom-if" if="[[reviewer.group]]">
+                <span>[[reviewer.group.name]] (group)</span>
+              </template>
+            </div>
+          </template>
+        </div>
+      </div>
+      <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
+          hidden$="[[_showInput]]">Add reviewer</gr-button>
+    </div>
+  </template>
+  <script src="gr-reviewer-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
new file mode 100644
index 0000000..00fc12e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -0,0 +1,344 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-reviewer-list',
+
+    properties: {
+      change: Object,
+      mutable: {
+        type: Boolean,
+        value: false,
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      suggestFrom: {
+        type: Number,
+        value: 3,
+      },
+
+      _reviewers: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _autocompleteData: {
+        type: Array,
+        value: function() { return []; },
+        observer: '_autocompleteDataChanged',
+      },
+      _inputVal: {
+        type: String,
+        value: '',
+        observer: '_inputValChanged',
+      },
+      _inputRequestHandle: Number,
+      _inputRequestTimeout: {
+        type: Number,
+        value: 250,
+      },
+      _showInput: {
+        type: Boolean,
+        value: false,
+      },
+      _hideAutocomplete: {
+        type: Boolean,
+        value: true,
+        observer: '_hideAutocompleteChanged',
+      },
+      _selectedIndex: {
+        type: Number,
+        value: 0,
+      },
+      _boundBodyClickHandler: {
+        type: Function,
+        value: function() {
+          return this._handleBodyClick.bind(this);
+        },
+      },
+
+      // Used for testing.
+      _lastAutocompleteRequest: Object,
+      _xhrPromise: Object,
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    observers: [
+      '_reviewersChanged(change.reviewers.*, change.owner)',
+    ],
+
+    detached: function() {
+      this._clearInputRequestHandle();
+    },
+
+    _clearInputRequestHandle: function() {
+      if (this._inputRequestHandle != null) {
+        this.cancelAsync(this._inputRequestHandle);
+        this._inputRequestHandle = null;
+      }
+    },
+
+    _reviewersChanged: function(changeRecord, owner) {
+      var result = [];
+      var reviewers = changeRecord.base;
+      for (var key in reviewers) {
+        if (key == 'REVIEWER' || key == 'CC') {
+          result = result.concat(reviewers[key]);
+        }
+      }
+      this._reviewers = result.filter(function(reviewer) {
+        return reviewer._account_id != owner._account_id;
+      });
+    },
+
+    _computeCanRemoveReviewer: function(reviewer, mutable) {
+      if (!mutable) { return false; }
+
+      for (var i = 0; i < this.change.removable_reviewers.length; i++) {
+        if (this.change.removable_reviewers[i]._account_id ==
+            reviewer._account_id) {
+          return true;
+        }
+      }
+      return false;
+    },
+
+    _computeAutocompleteURL: function(change) {
+      return '/changes/' + change._number + '/suggest_reviewers';
+    },
+
+    _computeAutocompleteParams: function(inputVal) {
+      return {
+        n: 10,  // Return max 10 results
+        q: inputVal,
+      };
+    },
+
+    _computeSelected: function(index, selectedIndex) {
+      return index == selectedIndex;
+    },
+
+    _handleResponse: function(e) {
+      this._autocompleteData = e.detail.response.filter(function(reviewer) {
+        var account = reviewer.account;
+        if (!account) { return true; }
+        for (var i = 0; i < this._reviewers.length; i++) {
+          if (account._account_id == this.change.owner._account_id ||
+              account._account_id == this._reviewers[i]._account_id) {
+            return false;
+          }
+        }
+        return true;
+      }, this);
+    },
+
+    _handleBodyClick: function(e) {
+      var eventPath = Polymer.dom(e).path;
+      for (var i = 0; i < eventPath.length; i++) {
+        if (eventPath[i] == this) {
+          return;
+        }
+      }
+      this._selectedIndex = -1;
+      this._autocompleteData = [];
+    },
+
+    _handleRemoveTap: function(e) {
+      e.preventDefault();
+      var target = Polymer.dom(e).rootTarget;
+      var accountID = parseInt(target.getAttribute('data-account-id'), 10);
+      this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
+        var reviewers = this.change.reviewers;
+        ['REVIEWER', 'CC'].forEach(function(type) {
+          reviewers[type] = reviewers[type] || [];
+          for (var i = 0; i < reviewers[type].length; i++) {
+            if (reviewers[type][i]._account_id == accountID) {
+              this.splice('change.reviewers.' + type, i, 1);
+              break;
+            }
+          }
+        }, this);
+      }.bind(this)).catch(function(err) {
+        alert('Oops. Something went wrong. Check the console and bug the ' +
+            'PolyGerrit team for assistance.');
+        throw err;
+      }.bind(this));
+    },
+
+    _handleAddTap: function(e) {
+      e.preventDefault();
+      this._showInput = true;
+      this.$.input.focus();
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this._cancel();
+    },
+
+    _handleMouseEnterItem: function(e) {
+      this._selectedIndex =
+          parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10);
+    },
+
+    _handleItemTap: function(e) {
+      var reviewerEl;
+      var eventPath = Polymer.dom(e).path;
+      for (var i = 0; i < eventPath.length; i++) {
+        var el = eventPath[i];
+        if (el.classList && el.classList.contains('reviewer')) {
+          reviewerEl = el;
+          break;
+        }
+      }
+      this._selectedIndex =
+          parseInt(reviewerEl.getAttribute('data-index'), 10);
+      this._sendAddRequest();
+    },
+
+    _autocompleteDataChanged: function(data) {
+      this._hideAutocomplete = data.length == 0;
+    },
+
+    _hideAutocompleteChanged: function(hidden) {
+      if (hidden) {
+        document.body.removeEventListener('click',
+            this._boundBodyClickHandler);
+        this._selectedIndex = -1;
+      } else {
+        document.body.addEventListener('click', this._boundBodyClickHandler);
+        this._selectedIndex = 0;
+      }
+    },
+
+    _inputValChanged: function(val) {
+      var sendRequest = function() {
+        if (this.disabled || val == null || val.trim().length == 0) {
+          return;
+        }
+        if (val.length < this.suggestFrom) {
+          this._clearInputRequestHandle();
+          this._hideAutocomplete = true;
+          this._selectedIndex = -1;
+          return;
+        }
+        this._lastAutocompleteRequest =
+            this.$.autocompleteXHR.generateRequest();
+      }.bind(this);
+
+      this._clearInputRequestHandle();
+      if (this._inputRequestTimeout == 0) {
+        sendRequest();
+      } else {
+        this._inputRequestHandle =
+            this.async(sendRequest, this._inputRequestTimeout);
+      }
+    },
+
+    _handleKey: function(e) {
+      if (this._hideAutocomplete) {
+        if (e.keyCode == 27) {  // 'esc'
+          e.preventDefault();
+          this._cancel();
+        }
+        return;
+      }
+
+      switch (e.keyCode) {
+        case 38:  // 'up':
+          e.preventDefault();
+          this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
+          break;
+        case 40:  // 'down'
+          e.preventDefault();
+          this._selectedIndex = Math.min(this._selectedIndex + 1,
+                                         this._autocompleteData.length - 1);
+          break;
+        case 27:  // 'esc'
+          e.preventDefault();
+          this._hideAutocomplete = true;
+          break;
+        case 13:  // 'enter'
+          e.preventDefault();
+          this._sendAddRequest();
+          break;
+      }
+    },
+
+    _cancel: function() {
+      this._showInput = false;
+      this._selectedIndex = 0;
+      this._inputVal = '';
+      this._autocompleteData = [];
+      this.$.addReviewer.focus();
+    },
+
+    _sendAddRequest: function() {
+      this._clearInputRequestHandle();
+
+      var reviewerID;
+      var reviewer = this._autocompleteData[this._selectedIndex];
+      if (reviewer.account) {
+        reviewerID = reviewer.account._account_id;
+      } else if (reviewer.group) {
+        reviewerID = reviewer.group.id;
+      }
+      this._autocompleteData = [];
+      this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
+        this.change.reviewers.CC = this.change.reviewers.CC || [];
+        req.response.reviewers.forEach(function(r) {
+          this.push('change.removable_reviewers', r);
+          this.push('change.reviewers.CC', r);
+        }, this);
+        this._inputVal = '';
+        this.$.input.focus();
+      }.bind(this)).catch(function(err) {
+        // TODO(andybons): Use the message returned by the server.
+        alert('Unable to add ' + reviewerID + ' as a reviewer.');
+        throw err;
+      }.bind(this));
+    },
+
+    _send: function(method, url, reviewerID) {
+      this.disabled = true;
+      var request = document.createElement('gr-request');
+      var opts = {
+        method: method,
+        url: url,
+      };
+      if (reviewerID) {
+        opts.body = {reviewer: reviewerID};
+      }
+      this._xhrPromise = request.send(opts);
+      var enableEl = function() { this.disabled = false; }.bind(this);
+      this._xhrPromise.then(enableEl).catch(enableEl);
+      return this._xhrPromise;
+    },
+
+    _restEndpoint: function(id) {
+      var path = '/changes/' + this.change._number + '/reviewers';
+      if (id) {
+        path += '/' + id;
+      }
+      return path;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-reviewer-list-test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
similarity index 95%
rename from polygerrit-ui/app/test/gr-reviewer-list-test.html
rename to polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 860b138..898d328 100644
--- a/polygerrit-ui/app/test/gr-reviewer-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-list</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-reviewer-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-reviewer-list.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
new file mode 100644
index 0000000..ce7faae
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -0,0 +1,54 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-diff-comment/gr-diff-comment.html">
+
+<dom-module id="gr-diff-comment-thread">
+  <template>
+    <style>
+      :host {
+        display: block;
+        white-space: normal;
+      }
+      gr-diff-comment {
+        border-left: 1px solid #ddd;
+      }
+      gr-diff-comment:first-of-type {
+        border-top: 1px solid #ddd;
+      }
+      gr-diff-comment:last-of-type {
+        border-bottom: 1px solid #ddd;
+      }
+    </style>
+    <div id="container">
+      <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
+        <gr-diff-comment
+            comment="{{comment}}"
+            change-num="[[changeNum]]"
+            patch-num="[[patchNum]]"
+            draft="[[comment.__draft]]"
+            show-actions="[[showActions]]"
+            project-config="[[projectConfig]]"
+            on-height-change="_handleCommentHeightChange"
+            on-reply="_handleCommentReply"
+            on-discard="_handleCommentDiscard"
+            on-done="_handleCommentDone"></gr-diff-comment>
+      </template>
+    </div>
+  </template>
+  <script src="gr-diff-comment-thread.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
new file mode 100644
index 0000000..32c8313
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -0,0 +1,214 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-comment-thread',
+
+    /**
+     * Fired when the height of the thread changes.
+     *
+     * @event height-change
+     */
+
+    /**
+     * Fired when the thread should be discarded.
+     *
+     * @event discard
+     */
+
+    properties: {
+      changeNum: String,
+      comments: {
+        type: Array,
+        value: function() { return []; },
+      },
+      patchNum: String,
+      path: String,
+      showActions: Boolean,
+      projectConfig: Object,
+
+      _boundWindowResizeHandler: {
+        type: Function,
+        value: function() { return this._handleWindowResize.bind(this); }
+      },
+      _lastHeight: Number,
+      _orderedComments: Array,
+    },
+
+    get naturalHeight() {
+      return this.$.container.offsetHeight;
+    },
+
+    observers: [
+      '_commentsChanged(comments.splices)',
+    ],
+
+    attached: function() {
+      window.addEventListener('resize', this._boundWindowResizeHandler);
+    },
+
+    detached: function() {
+      window.removeEventListener('resize', this._boundWindowResizeHandler);
+    },
+
+    _handleWindowResize: function(e) {
+      this._heightChanged();
+    },
+
+    _commentsChanged: function(changeRecord) {
+      this._orderedComments = this._sortedComments(this.comments);
+    },
+
+    _sortedComments: function(comments) {
+      comments.sort(function(c1, c2) {
+        var c1Date = c1.__date || util.parseDate(c1.updated);
+        var c2Date = c2.__date || util.parseDate(c2.updated);
+        return c1Date - c2Date;
+      });
+
+      var commentIDToReplies = {};
+      var topLevelComments = [];
+      for (var i = 0; i < comments.length; i++) {
+        var c = comments[i];
+        if (c.in_reply_to) {
+          if (commentIDToReplies[c.in_reply_to] == null) {
+            commentIDToReplies[c.in_reply_to] = [];
+          }
+          commentIDToReplies[c.in_reply_to].push(c);
+        } else {
+          topLevelComments.push(c);
+        }
+      }
+      var results = [];
+      for (var i = 0; i < topLevelComments.length; i++) {
+        this._visitComment(topLevelComments[i], commentIDToReplies, results);
+      }
+      return results;
+    },
+
+    _visitComment: function(parent, commentIDToReplies, results) {
+      results.push(parent);
+
+      var replies = commentIDToReplies[parent.id];
+      if (!replies) { return; }
+      for (var i = 0; i < replies.length; i++) {
+        this._visitComment(replies[i], commentIDToReplies, results);
+      }
+    },
+
+    _handleCommentHeightChange: function(e) {
+      e.stopPropagation();
+      this._heightChanged();
+    },
+
+    _handleCommentReply: function(e) {
+      var comment = e.detail.comment;
+      var quoteStr;
+      if (e.detail.quote) {
+        var msg = comment.message;
+        var quoteStr = msg.split('\n').map(
+            function(line) { return ' > ' + line; }).join('\n') + '\n\n';
+      }
+      var reply =
+          this._newReply(comment.id, comment.line, this.path, quoteStr);
+      this.push('comments', reply);
+
+      // Allow the reply to render in the dom-repeat.
+      this.async(function() {
+        var commentEl = this._commentElWithDraftID(reply.__draftID);
+        commentEl.editing = true;
+        this.async(this._heightChanged.bind(this), 1);
+      }.bind(this), 1);
+    },
+
+    _handleCommentDone: function(e) {
+      var comment = e.detail.comment;
+      var reply = this._newReply(comment.id, comment.line, this.path, 'Done');
+      this.push('comments', reply);
+
+      // Allow the reply to render in the dom-repeat.
+      this.async(function() {
+        var commentEl = this._commentElWithDraftID(reply.__draftID);
+        commentEl.save();
+        this.async(this._heightChanged.bind(this), 1);
+      }.bind(this), 1);
+    },
+
+    _commentElWithDraftID: function(draftID) {
+      var commentEls =
+          Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
+      for (var i = 0; i < commentEls.length; i++) {
+        if (commentEls[i].comment.__draftID == draftID) {
+          return commentEls[i];
+        }
+      }
+      return null;
+    },
+
+    _newReply: function(inReplyTo, line, path, opt_message) {
+      var c = {
+        __draft: true,
+        __draftID: Math.random().toString(36),
+        __date: new Date(),
+        line: line,
+        path: path,
+        in_reply_to: inReplyTo,
+      };
+      if (opt_message != null) {
+        c.message = opt_message;
+      }
+      return c;
+    },
+
+    _handleCommentDiscard: function(e) {
+      // TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady
+      // DOM, it respects the bubbles property.
+      // https://github.com/Polymer/polymer/issues/3226
+      e.stopPropagation();
+      var diffCommentEl = Polymer.dom(e).rootTarget;
+      var idx = this._indexOf(diffCommentEl.comment, this.comments);
+      if (idx == -1) {
+        throw Error('Cannot find comment ' +
+            JSON.stringify(diffCommentEl.comment));
+      }
+      this.splice('comments', idx, 1);
+      if (this.comments.length == 0) {
+        this.fire('discard', null, {bubbles: false});
+        return;
+      }
+      this.async(this._heightChanged.bind(this), 1);
+    },
+
+    _heightChanged: function() {
+      var height = this.$.container.offsetHeight;
+      if (height == this._lastHeight) { return; }
+
+      this.fire('height-change', {height: height}, {bubbles: false});
+      this._lastHeight = height;
+    },
+
+    _indexOf: function(comment, arr) {
+      for (var i = 0; i < arr.length; i++) {
+        var c = arr[i];
+        if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
+            (c.id != null && c.id == comment.id)) {
+          return i;
+        }
+      }
+      return -1;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-comment-thread-test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-diff-comment-thread-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 1218d19..52ad066 100644
--- a/polygerrit-ui/app/test/gr-diff-comment-thread-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-comment-thread</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-comment-thread.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-comment-thread.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
new file mode 100644
index 0000000..ca6815b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -0,0 +1,153 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<dom-module id="gr-diff-comment">
+  <template>
+    <style>
+      :host {
+        background-color: #ffd;
+        display: block;
+        --iron-autogrow-textarea: {
+          padding: 2px;
+        };
+      }
+      :host([disabled]) {
+        pointer-events: none;
+      }
+      :host([disabled]) .container {
+        opacity: .5;
+      }
+      .header,
+      .message,
+      .actions {
+        padding: .5em .7em;
+      }
+      .header {
+        display: flex;
+        padding-bottom: 0;
+        font-family: 'Open Sans', sans-serif;
+      }
+      .headerLeft {
+        flex: 1;
+      }
+      .authorName,
+      .draftLabel {
+        font-weight: bold;
+      }
+      .draftLabel {
+        color: #999;
+        display: none;
+      }
+      .date {
+        justify-content: flex-end;
+        margin-left: 5px;
+      }
+      a.date:link,
+      a.date:visited {
+        color: #666;
+      }
+      .actions {
+        display: flex;
+        padding-top: 0;
+      }
+      .action {
+        margin-right: .5em;
+      }
+      .danger {
+        display: flex;
+        flex: 1;
+        justify-content: flex-end;
+      }
+      .editMessage {
+        display: none;
+        margin: .5em .7em;
+        width: calc(100% - 1.4em - 2px);
+      }
+      .danger .action {
+        margin-right: 0;
+      }
+      .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) {
+        display: none;
+      }
+      .draft .reply,
+      .draft .quote,
+      .draft .done {
+        display: none;
+      }
+      .draft .draftLabel {
+        display: inline;
+      }
+      .draft:not(.editing) .save,
+      .draft:not(.editing) .cancel {
+        display: none;
+      }
+      .editing .message,
+      .editing .reply,
+      .editing .quote,
+      .editing .done,
+      .editing .edit {
+        display: none;
+      }
+      .editing .editMessage {
+        background-color: #fff;
+        display: block;
+      }
+    </style>
+    <div class="container" id="container">
+      <div class="header" id="header">
+        <div class="headerLeft">
+          <span class="authorName">[[comment.author.name]]</span>
+          <span class="draftLabel">DRAFT</span>
+        </div>
+        <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
+          <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
+        </a>
+      </div>
+      <iron-autogrow-textarea
+          id="editTextarea"
+          class="editMessage"
+          disabled="{{disabled}}"
+          rows="4"
+          bind-value="{{_editDraft}}"
+          on-keyup="_handleTextareaKeyup"
+          on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
+      <gr-linked-text class="message"
+          pre
+          content="[[comment.message]]"
+          config="[[projectConfig.commentlinks]]"></gr-linked-text>
+      <div class="actions" hidden$="[[!showActions]]">
+        <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
+        <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
+        <gr-button class="action done" on-tap="_handleDone">Done</gr-button>
+        <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
+        <gr-button class="action save" on-tap="_handleSave"
+            disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
+        <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
+        <div class="danger">
+          <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
+        </div>
+      </div>
+    </div>
+  </template>
+  <script src="gr-diff-comment.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
new file mode 100644
index 0000000..ca0bedb
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -0,0 +1,247 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-comment',
+
+    /**
+     * Fired when the height of the comment changes.
+     *
+     * @event height-change
+     */
+
+    /**
+     * Fired when the Reply action is triggered.
+     *
+     * @event reply
+     */
+
+    /**
+     * Fired when the Done action is triggered.
+     *
+     * @event done
+     */
+
+    /**
+     * Fired when this comment is discarded.
+     *
+     * @event discard
+     */
+
+    properties: {
+      changeNum: String,
+      comment: {
+        type: Object,
+        notify: true,
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      draft: {
+        type: Boolean,
+        value: false,
+        observer: '_draftChanged',
+      },
+      editing: {
+        type: Boolean,
+        value: false,
+        observer: '_editingChanged',
+      },
+      patchNum: String,
+      showActions: Boolean,
+      projectConfig: Object,
+
+      _xhrPromise: Object,  // Used for testing.
+      _editDraft: String,
+    },
+
+    ready: function() {
+      this._editDraft = (this.comment && this.comment.message) || '';
+      this.editing = this._editDraft.length == 0;
+    },
+
+    attached: function() {
+      this._heightChanged();
+    },
+
+    save: function() {
+      this.comment.message = this._editDraft;
+      this.disabled = true;
+      var endpoint = this._restEndpoint(this.comment.id);
+      this._send('PUT', endpoint).then(function(req) {
+        this.disabled = false;
+        var comment = req.response;
+        comment.__draft = true;
+        // Maintain the ephemeral draft ID for identification by other
+        // elements.
+        if (this.comment.__draftID) {
+          comment.__draftID = this.comment.__draftID;
+        }
+        this.comment = comment;
+        this.editing = false;
+      }.bind(this)).catch(function(err) {
+        alert('Your draft couldn’t be saved. Check the console and contact ' +
+            'the PolyGerrit team for assistance.');
+        this.disabled = false;
+      }.bind(this));
+    },
+
+    _heightChanged: function() {
+      this.async(function() {
+        this.fire('height-change', {height: this.offsetHeight},
+            {bubbles: false});
+      }.bind(this));
+    },
+
+    _draftChanged: function(draft) {
+      this.$.container.classList.toggle('draft', draft);
+    },
+
+    _editingChanged: function(editing) {
+      this.$.container.classList.toggle('editing', editing);
+      if (editing) {
+        var textarea = this.$.editTextarea.textarea;
+        // Put the cursor at the end always.
+        textarea.selectionStart = textarea.value.length;
+        textarea.selectionEnd = textarea.selectionStart;
+        this.async(function() {
+          textarea.focus();
+        }.bind(this));
+      }
+      if (this.comment && this.comment.id) {
+        this.$$('.cancel').hidden = !editing;
+      }
+      this._heightChanged();
+    },
+
+    _computeLinkToComment: function(comment) {
+      return '#' + comment.line;
+    },
+
+    _computeSaveDisabled: function(draft) {
+      return draft == null || draft.trim() == '';
+    },
+
+    _handleTextareaKeyup: function(e) {
+      // TODO(andybons): This isn't always true, but I can't currently think
+      // of a better metric.
+      this._heightChanged();
+    },
+
+    _handleTextareaKeydown: function(e) {
+      if (e.keyCode == 27) {  // 'esc'
+        this._handleCancel(e);
+      }
+    },
+
+    _handleLinkTap: function(e) {
+      e.preventDefault();
+      var hash = this._computeLinkToComment(this.comment);
+      // Don't add the hash to the window history if it's already there.
+      // Otherwise you mess up expected back button behavior.
+      if (window.location.hash == hash) { return; }
+      // Change the URL but don’t trigger a nav event. Otherwise it will
+      // reload the page.
+      page.show(window.location.pathname + hash, null, false);
+    },
+
+    _handleReply: function(e) {
+      this._preventDefaultAndBlur(e);
+      this.fire('reply', {comment: this.comment}, {bubbles: false});
+    },
+
+    _handleQuote: function(e) {
+      this._preventDefaultAndBlur(e);
+      this.fire('reply', {comment: this.comment, quote: true},
+          {bubbles: false});
+    },
+
+    _handleDone: function(e) {
+      this._preventDefaultAndBlur(e);
+      this.fire('done', {comment: this.comment}, {bubbles: false});
+    },
+
+    _handleEdit: function(e) {
+      this._preventDefaultAndBlur(e);
+      this._editDraft = this.comment.message;
+      this.editing = true;
+    },
+
+    _handleSave: function(e) {
+      this._preventDefaultAndBlur(e);
+      this.save();
+    },
+
+    _handleCancel: function(e) {
+      this._preventDefaultAndBlur(e);
+      if (this.comment.message == null || this.comment.message.length == 0) {
+        this.fire('discard', null, {bubbles: false});
+        return;
+      }
+      this._editDraft = this.comment.message;
+      this.editing = false;
+    },
+
+    _handleDiscard: function(e) {
+      this._preventDefaultAndBlur(e);
+      if (!this.comment.__draft) {
+        throw Error('Cannot discard a non-draft comment.');
+      }
+      this.disabled = true;
+      var commentID = this.comment.id;
+      if (!commentID) {
+        this.fire('discard', null, {bubbles: false});
+        return;
+      }
+      this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
+        this.fire('discard', null, {bubbles: false});
+      }.bind(this)).catch(function(err) {
+        alert('Your draft couldn’t be deleted. Check the console and ' +
+            'contact the PolyGerrit team for assistance.');
+        this.disabled = false;
+      }.bind(this));
+    },
+
+    _preventDefaultAndBlur: function(e) {
+      e.preventDefault();
+      Polymer.dom(e).rootTarget.blur();
+    },
+
+    _send: function(method, url) {
+      var xhr = document.createElement('gr-request');
+      var opts = {
+        method: method,
+        url: url,
+      };
+      if (method == 'PUT' || method == 'POST') {
+        opts.body = this.comment;
+      }
+      this._xhrPromise = xhr.send(opts);
+      return this._xhrPromise;
+    },
+
+    _restEndpoint: function(id) {
+      var path = '/changes/' + this.changeNum + '/revisions/' +
+          this.patchNum + '/drafts';
+      if (id) {
+        path += '/' + id;
+      }
+      return path;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-comment-test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-diff-comment-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index e63db22..799dbf2 100644
--- a/polygerrit-ui/app/test/gr-diff-comment-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-comment</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-comment.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-comment.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
similarity index 66%
rename from polygerrit-ui/app/elements/gr-diff-preferences.html
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index e320620..b945a45 100644
--- a/polygerrit-ui/app/elements/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -14,9 +14,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-input/iron-input.html">
-<link rel="import" href="gr-button.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
 
 <dom-module id="gr-diff-preferences">
   <template>
@@ -106,65 +106,5 @@
       <gr-button on-tap="_handleCancel">Cancel</gr-button>
     </div>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-diff-preferences',
-
-      /**
-       * Fired when the user presses the save button.
-       *
-       * @event save
-       */
-
-      /**
-       * Fired when the user presses the cancel button.
-       *
-       * @event cancel
-       */
-
-      properties: {
-        prefs: {
-          type: Object,
-          notify: true,
-          value: function() { return {}; },
-        },
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-      },
-
-      observers: [
-        '_prefsChanged(prefs.*)',
-      ],
-
-      _prefsChanged: function(changeRecord) {
-        var prefs = changeRecord.base;
-        this.$.contextSelect.value = prefs.context;
-        this.$.showTabsInput.checked = prefs.show_tabs;
-      },
-
-      _handleContextSelectChange: function(e) {
-        var selectEl = Polymer.dom(e).rootTarget;
-        this.set('prefs.context', parseInt(selectEl.value, 10));
-      },
-
-      _handleShowTabsTap: function(e) {
-        this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked);
-      },
-
-      _handleSave: function() {
-        this.fire('save', null, {bubbles: false});
-      },
-
-      _handleCancel: function() {
-        this.fire('cancel', null, {bubbles: false});
-      },
-    });
-  })();
-  </script>
+  <script src="gr-diff-preferences.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
new file mode 100644
index 0000000..70d176e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -0,0 +1,72 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff-preferences',
+
+    /**
+     * Fired when the user presses the save button.
+     *
+     * @event save
+     */
+
+    /**
+     * Fired when the user presses the cancel button.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      prefs: {
+        type: Object,
+        notify: true,
+        value: function() { return {}; },
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+    },
+
+    observers: [
+      '_prefsChanged(prefs.*)',
+    ],
+
+    _prefsChanged: function(changeRecord) {
+      var prefs = changeRecord.base;
+      this.$.contextSelect.value = prefs.context;
+      this.$.showTabsInput.checked = prefs.show_tabs;
+    },
+
+    _handleContextSelectChange: function(e) {
+      var selectEl = Polymer.dom(e).rootTarget;
+      this.set('prefs.context', parseInt(selectEl.value, 10));
+    },
+
+    _handleShowTabsTap: function(e) {
+      this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked);
+    },
+
+    _handleSave: function() {
+      this.fire('save', null, {bubbles: false});
+    },
+
+    _handleCancel: function() {
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-preferences-test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
similarity index 86%
rename from polygerrit-ui/app/test/gr-diff-preferences-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 9256f2f..2d86a05 100644
--- a/polygerrit-ui/app/test/gr-diff-preferences-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-preferences</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-preferences.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-preferences.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
new file mode 100644
index 0000000..972dc2d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
@@ -0,0 +1,97 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
+
+<dom-module id="gr-diff-side">
+  <template>
+    <style>
+      :host,
+      .container {
+        display: flex;
+        flex: 0 0 auto;
+      }
+      .lineNum:before,
+      .code:before {
+        /* To ensure the height is non-zero in these elements, a
+           zero-width space is set as its content. The character
+           itself doesn't matter. Just that there is something
+           there. */
+        content: '\200B';
+      }
+      .lineNum {
+        background-color: #eee;
+        color: #666;
+        padding: 0 .75em;
+        text-align: right;
+      }
+      .canComment .lineNum {
+        cursor: pointer;
+        text-decoration: underline;
+      }
+      .canComment .lineNum:hover {
+        background-color: #ccc;
+      }
+      .lightHighlight {
+        background-color: var(--light-highlight-color);
+      }
+      hl,
+      .darkHighlight {
+        background-color: var(--dark-highlight-color);
+      }
+      .br:after {
+        /* Line feed */
+        content: '\A';
+      }
+      .tab {
+        display: inline-block;
+      }
+      .tab.withIndicator:before {
+        color: #C62828;
+        /* >> character */
+        content: '\00BB';
+      }
+      .numbers,
+      .content {
+        white-space: pre;
+      }
+      .numbers .filler {
+        background-color: #eee;
+      }
+      .contextControl {
+        background-color: #fef;
+      }
+      .contextControl a:link,
+      .contextControl a:visited {
+        display: block;
+        text-decoration: none;
+      }
+      .numbers .contextControl {
+        padding: 0 .75em;
+        text-align: right;
+      }
+      .content .contextControl {
+        text-align: center;
+      }
+    </style>
+    <div class$="[[_computeContainerClass(canComment)]]">
+      <div class="numbers" id="numbers"></div>
+      <div class="content" id="content"></div>
+    </div>
+  </template>
+  <script src="gr-diff-side.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
new file mode 100644
index 0000000..518da3e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
@@ -0,0 +1,613 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var CharCode = {
+    LESS_THAN: '<'.charCodeAt(0),
+    GREATER_THAN: '>'.charCodeAt(0),
+    AMPERSAND: '&'.charCodeAt(0),
+    SEMICOLON: ';'.charCodeAt(0),
+  };
+
+  var TAB_REGEX = /\t/g;
+
+  Polymer({
+    is: 'gr-diff-side',
+
+    /**
+     * Fired when an expand context control is clicked.
+     *
+     * @event expand-context
+     */
+
+    /**
+     * Fired when a thread's height is changed.
+     *
+     * @event thread-height-change
+     */
+
+    /**
+     * Fired when a draft should be added.
+     *
+     * @event add-draft
+     */
+
+    /**
+     * Fired when a thread is removed.
+     *
+     * @event remove-thread
+     */
+
+    properties: {
+      canComment: {
+        type: Boolean,
+        value: false,
+      },
+      content: {
+        type: Array,
+        notify: true,
+        observer: '_contentChanged',
+      },
+      prefs: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      changeNum: String,
+      patchNum: String,
+      path: String,
+      projectConfig: {
+        type: Object,
+        observer: '_projectConfigChanged',
+      },
+
+      _lineFeedHTML: {
+        type: String,
+        value: '<span class="style-scope gr-diff-side br"></span>',
+        readOnly: true,
+      },
+      _highlightStartTag: {
+        type: String,
+        value: '<hl class="style-scope gr-diff-side">',
+        readOnly: true,
+      },
+      _highlightEndTag: {
+        type: String,
+        value: '</hl>',
+        readOnly: true,
+      },
+      _diffChunkLineNums: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _commentThreadLineNums: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _focusedLineNum: {
+        type: Number,
+        value: 1,
+      },
+    },
+
+    listeners: {
+      'tap': '_tapHandler',
+    },
+
+    observers: [
+      '_prefsChanged(prefs.*)',
+    ],
+
+    rowInserted: function(index) {
+      this.renderLineIndexRange(index, index);
+      this._updateDOMIndices();
+      this._updateJumpIndices();
+    },
+
+    rowRemoved: function(index) {
+      var removedEls = Polymer.dom(this.root).querySelectorAll(
+          '[data-index="' + index + '"]');
+      for (var i = 0; i < removedEls.length; i++) {
+        removedEls[i].parentNode.removeChild(removedEls[i]);
+      }
+      this._updateDOMIndices();
+      this._updateJumpIndices();
+    },
+
+    rowUpdated: function(index) {
+      var removedEls = Polymer.dom(this.root).querySelectorAll(
+          '[data-index="' + index + '"]');
+      for (var i = 0; i < removedEls.length; i++) {
+        removedEls[i].parentNode.removeChild(removedEls[i]);
+      }
+      this.renderLineIndexRange(index, index);
+    },
+
+    scrollToLine: function(lineNum) {
+      if (isNaN(lineNum) || lineNum < 1) { return; }
+
+      var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
+      if (!el) { return; }
+
+      // Calculate where the line is relative to the window.
+      var top = el.offsetTop;
+      for (var offsetParent = el.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+
+      // Scroll the element to the middle of the window. Dividing by a third
+      // instead of half the inner height feels a bit better otherwise the
+      // element appears to be below the center of the window even when it
+      // isn't.
+      window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
+    },
+
+    scrollToNextDiffChunk: function() {
+      this._scrollToNextChunkOrThread(this._diffChunkLineNums);
+    },
+
+    scrollToPreviousDiffChunk: function() {
+      this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
+    },
+
+    scrollToNextCommentThread: function() {
+      this._scrollToNextChunkOrThread(this._commentThreadLineNums);
+    },
+
+    scrollToPreviousCommentThread: function() {
+      this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
+    },
+
+    renderLineIndexRange: function(startIndex, endIndex) {
+      this._render(this.content, startIndex, endIndex);
+    },
+
+    hideElementsWithIndex: function(index) {
+      var els = Polymer.dom(this.root).querySelectorAll(
+          '[data-index="' + index + '"]');
+      for (var i = 0; i < els.length; i++) {
+        els[i].setAttribute('hidden', true);
+      }
+    },
+
+    getRowHeight: function(index) {
+      var row = this.content[index];
+      // Filler elements should not be taken into account when determining
+      // height calculations.
+      if (row.type == 'FILLER') {
+        return 0;
+      }
+      if (row.height != null) {
+        return row.height;
+      }
+
+      var selector = '[data-index="' + index + '"]';
+      var els = Polymer.dom(this.root).querySelectorAll(selector);
+      if (els.length != 2) {
+        throw Error('Rows should only consist of two elements');
+      }
+      return Math.max(els[0].offsetHeight, els[1].offsetHeight);
+    },
+
+    getRowNaturalHeight: function(index) {
+      var contentEl = this.$$('.content [data-index="' + index + '"]');
+      return contentEl.naturalHeight || contentEl.offsetHeight;
+    },
+
+    setRowNaturalHeight: function(index) {
+      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
+      var contentEl = this.$$('.content [data-index="' + index + '"]');
+      contentEl.style.height = null;
+      var height = contentEl.offsetHeight;
+      lineEl.style.height = height + 'px';
+      this.content[index].height = height;
+      return height;
+    },
+
+    setRowHeight: function(index, height) {
+      var selector = '[data-index="' + index + '"]';
+      var els = Polymer.dom(this.root).querySelectorAll(selector);
+      for (var i = 0; i < els.length; i++) {
+        els[i].style.height = height + 'px';
+      }
+      this.content[index].height = height;
+    },
+
+    _scrollToNextChunkOrThread: function(lineNums) {
+      for (var i = 0; i < lineNums.length; i++) {
+        if (lineNums[i] > this._focusedLineNum) {
+          this._focusedLineNum = lineNums[i];
+          this.scrollToLine(this._focusedLineNum);
+          return;
+        }
+      }
+    },
+
+    _scrollToPreviousChunkOrThread: function(lineNums) {
+      for (var i = lineNums.length - 1; i >= 0; i--) {
+        if (this._focusedLineNum > lineNums[i]) {
+          this._focusedLineNum = lineNums[i];
+          this.scrollToLine(this._focusedLineNum);
+          return;
+        }
+      }
+    },
+
+    _updateJumpIndices: function() {
+      this._commentThreadLineNums = [];
+      this._diffChunkLineNums = [];
+      var inHighlight = false;
+      for (var i = 0; i < this.content.length; i++) {
+        switch (this.content[i].type) {
+          case 'COMMENT_THREAD':
+            this._commentThreadLineNums.push(
+                this.content[i].comments[0].line);
+            break;
+          case 'CODE':
+            // Only grab the first line of the highlighted chunk.
+            if (!inHighlight && this.content[i].highlight) {
+              this._diffChunkLineNums.push(this.content[i].lineNum);
+              inHighlight = true;
+            } else if (!this.content[i].highlight) {
+              inHighlight = false;
+            }
+            break;
+        }
+      }
+    },
+
+    _updateDOMIndices: function() {
+      // There is no way to select elements with a data-index greater than a
+      // given value. For now, just update all DOM elements.
+      var lineEls = Polymer.dom(this.root).querySelectorAll(
+          '.numbers [data-index]');
+      var contentEls = Polymer.dom(this.root).querySelectorAll(
+          '.content [data-index]');
+      if (lineEls.length != contentEls.length) {
+        throw Error(
+            'There must be the same number of line and content elements');
+      }
+      var index = 0;
+      for (var i = 0; i < this.content.length; i++) {
+        if (this.content[i].hidden) { continue; }
+
+        lineEls[index].setAttribute('data-index', i);
+        contentEls[index].setAttribute('data-index', i);
+        index++;
+      }
+    },
+
+    _prefsChanged: function(changeRecord) {
+      var prefs = changeRecord.base;
+      this.$.content.style.width = prefs.line_length + 'ch';
+    },
+
+    _projectConfigChanged: function(projectConfig) {
+      var threadEls =
+          Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
+      for (var i = 0; i < threadEls.length; i++) {
+        threadEls[i].projectConfig = projectConfig;
+      }
+    },
+
+    _contentChanged: function(diff) {
+      this._clearChildren(this.$.numbers);
+      this._clearChildren(this.$.content);
+      this._render(diff, 0, diff.length - 1);
+      this._updateJumpIndices();
+    },
+
+    _computeContainerClass: function(canComment) {
+      return 'container' + (canComment ? ' canComment' : '');
+    },
+
+    _tapHandler: function(e) {
+      var lineEl = Polymer.dom(e).rootTarget;
+      if (!this.canComment || !lineEl.classList.contains('lineNum')) {
+        return;
+      }
+
+      e.preventDefault();
+      var index = parseInt(lineEl.getAttribute('data-index'), 10);
+      var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
+      this.fire('add-draft', {
+        index: index,
+        line: line
+      }, {bubbles: false});
+    },
+
+    _clearChildren: function(el) {
+      while (el.firstChild) {
+        el.removeChild(el.firstChild);
+      }
+    },
+
+    _handleContextControlClick: function(context, e) {
+      e.preventDefault();
+      this.fire('expand-context', {context: context}, {bubbles: false});
+    },
+
+    _render: function(diff, startIndex, endIndex) {
+      var beforeLineEl;
+      var beforeContentEl;
+      if (endIndex != diff.length - 1) {
+        beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
+        beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
+        if (!beforeLineEl && !beforeContentEl) {
+          // `endIndex` may be present within the model, but not in the DOM.
+          // Insert it before its successive element.
+          beforeLineEl = this.$$(
+              '.numbers [data-index="' + (endIndex + 1) + '"]');
+          beforeContentEl = this.$$(
+              '.content [data-index="' + (endIndex + 1) + '"]');
+        }
+      }
+
+      for (var i = startIndex; i <= endIndex; i++) {
+        if (diff[i].hidden) { continue; }
+
+        switch (diff[i].type) {
+          case 'CODE':
+            this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
+            break;
+          case 'FILLER':
+            this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
+            break;
+          case 'CONTEXT_CONTROL':
+            this._renderContextControl(diff[i], i, beforeLineEl,
+                beforeContentEl);
+            break;
+          case 'COMMENT_THREAD':
+            this._renderCommentThread(diff[i], i, beforeLineEl,
+                beforeContentEl);
+            break;
+        }
+      }
+    },
+
+    _handleCommentThreadHeightChange: function(e) {
+      var threadEl = Polymer.dom(e).rootTarget;
+      var index = parseInt(threadEl.getAttribute('data-index'), 10);
+      this.content[index].height = e.detail.height;
+      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
+      lineEl.style.height = e.detail.height + 'px';
+      this.fire('thread-height-change', {
+        index: index,
+        height: e.detail.height,
+      }, {bubbles: false});
+    },
+
+    _handleCommentThreadDiscard: function(e) {
+      var threadEl = Polymer.dom(e).rootTarget;
+      var index = parseInt(threadEl.getAttribute('data-index'), 10);
+      this.fire('remove-thread', {index: index}, {bubbles: false});
+    },
+
+    _renderCommentThread: function(thread, index, beforeLineEl,
+        beforeContentEl) {
+      var lineEl = this._createElement('div', 'commentThread');
+      lineEl.classList.add('filler');
+      lineEl.setAttribute('data-index', index);
+      var threadEl = document.createElement('gr-diff-comment-thread');
+      threadEl.addEventListener('height-change',
+          this._handleCommentThreadHeightChange.bind(this));
+      threadEl.addEventListener('discard',
+          this._handleCommentThreadDiscard.bind(this));
+      threadEl.setAttribute('data-index', index);
+      threadEl.changeNum = this.changeNum;
+      threadEl.patchNum = thread.patchNum || this.patchNum;
+      threadEl.path = this.path;
+      threadEl.comments = thread.comments;
+      threadEl.showActions = this.canComment;
+      threadEl.projectConfig = this.projectConfig;
+
+      this.$.numbers.insertBefore(lineEl, beforeLineEl);
+      this.$.content.insertBefore(threadEl, beforeContentEl);
+    },
+
+    _renderContextControl: function(control, index, beforeLineEl,
+        beforeContentEl) {
+      var lineEl = this._createElement('div', 'contextControl');
+      lineEl.setAttribute('data-index', index);
+      lineEl.textContent = '@@';
+      var contentEl = this._createElement('div', 'contextControl');
+      contentEl.setAttribute('data-index', index);
+      var a = this._createElement('a');
+      a.href = '#';
+      a.textContent = 'Show ' + control.numLines + ' common ' +
+          (control.numLines == 1 ? 'line' : 'lines') + '...';
+      a.addEventListener('click',
+          this._handleContextControlClick.bind(this, control));
+      contentEl.appendChild(a);
+
+      this.$.numbers.insertBefore(lineEl, beforeLineEl);
+      this.$.content.insertBefore(contentEl, beforeContentEl);
+    },
+
+    _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
+      var lineFillerEl = this._createElement('div', 'filler');
+      lineFillerEl.setAttribute('data-index', index);
+      var fillerEl = this._createElement('div', 'filler');
+      fillerEl.setAttribute('data-index', index);
+      var numLines = filler.numLines || 1;
+
+      lineFillerEl.textContent = '\n'.repeat(numLines);
+      for (var i = 0; i < numLines; i++) {
+        var newlineEl = this._createElement('span', 'br');
+        fillerEl.appendChild(newlineEl);
+      }
+
+      this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
+      this.$.content.insertBefore(fillerEl, beforeContentEl);
+    },
+
+    _renderCode: function(code, index, beforeLineEl, beforeContentEl) {
+      var lineNumEl = this._createElement('div', 'lineNum');
+      lineNumEl.setAttribute('data-line-num', code.lineNum);
+      lineNumEl.setAttribute('data-index', index);
+      var numLines = code.numLines || 1;
+      lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
+
+      var contentEl = this._createElement('div', 'code');
+      contentEl.setAttribute('data-line-num', code.lineNum);
+      contentEl.setAttribute('data-index', index);
+
+      if (code.highlight) {
+        contentEl.classList.add(code.intraline.length > 0 ?
+            'lightHighlight' : 'darkHighlight');
+      }
+
+      var html = util.escapeHTML(code.content);
+      if (code.highlight && code.intraline.length > 0) {
+        html = this._addIntralineHighlights(code.content, html,
+            code.intraline);
+      }
+      if (numLines > 1) {
+        html = this._addNewLines(code.content, html, numLines);
+      }
+      html = this._addTabWrappers(code.content, html);
+
+      // If the html is equivalent to the text then it didn't get highlighted
+      // or escaped. Use textContent which is faster than innerHTML.
+      if (code.content == html) {
+        contentEl.textContent = code.content;
+      } else {
+        contentEl.innerHTML = html;
+      }
+
+      this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
+      this.$.content.insertBefore(contentEl, beforeContentEl);
+    },
+
+    // Advance `index` by the appropriate number of characters that would
+    // represent one source code character and return that index. For
+    // example, for source code '<span>' the escaped html string is
+    // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
+    // return 4, since &lt; maps to one source code character ('<').
+    _advanceChar: function(html, index) {
+      // Any tags don't count as characters
+      while (index < html.length &&
+             html.charCodeAt(index) == CharCode.LESS_THAN) {
+        while (index < html.length &&
+               html.charCodeAt(index) != CharCode.GREATER_THAN) {
+          index++;
+        }
+        index++;  // skip the ">" itself
+      }
+      // An HTML entity (e.g., &lt;) counts as one character.
+      if (index < html.length &&
+          html.charCodeAt(index) == CharCode.AMPERSAND) {
+        while (index < html.length &&
+               html.charCodeAt(index) != CharCode.SEMICOLON) {
+          index++;
+        }
+      }
+      return index + 1;
+    },
+
+    _addIntralineHighlights: function(content, html, highlights) {
+      var startTag = this._highlightStartTag;
+      var endTag = this._highlightEndTag;
+
+      for (var i = 0; i < highlights.length; i++) {
+        var hl = highlights[i];
+
+        var htmlStartIndex = 0;
+        for (var j = 0; j < hl.startIndex; j++) {
+          htmlStartIndex = this._advanceChar(html, htmlStartIndex);
+        }
+
+        var htmlEndIndex = 0;
+        if (hl.endIndex != null) {
+          for (var j = 0; j < hl.endIndex; j++) {
+            htmlEndIndex = this._advanceChar(html, htmlEndIndex);
+          }
+        } else {
+          // If endIndex isn't present, continue to the end of the line.
+          htmlEndIndex = html.length;
+        }
+        // The start and end indices could be the same if a highlight is meant
+        // to start at the end of a line and continue onto the next one.
+        // Ignore it.
+        if (htmlStartIndex != htmlEndIndex) {
+          html = html.slice(0, htmlStartIndex) + startTag +
+                html.slice(htmlStartIndex, htmlEndIndex) + endTag +
+                html.slice(htmlEndIndex);
+        }
+      }
+      return html;
+    },
+
+    _addNewLines: function(content, html, numLines) {
+      var htmlIndex = 0;
+      var indices = [];
+      var numChars = 0;
+      for (var i = 0; i < content.length; i++) {
+        if (numChars > 0 && numChars % this.prefs.line_length == 0) {
+          indices.push(htmlIndex);
+        }
+        htmlIndex = this._advanceChar(html, htmlIndex);
+        if (content[i] == '\t') {
+          numChars += this.prefs.tab_size;
+        } else {
+          numChars++;
+        }
+      }
+      var result = html;
+      var linesLeft = numLines;
+      // Since the result string is being altered in place, start from the end
+      // of the string so that the insertion indices are not affected as the
+      // result string changes.
+      for (var i = indices.length - 1; i >= 0; i--) {
+        result = result.slice(0, indices[i]) + this._lineFeedHTML +
+            result.slice(indices[i]);
+        linesLeft--;
+      }
+      // numLines is the total number of lines this code block should take up.
+      // Fill in the remaining ones.
+      for (var i = 0; i < linesLeft; i++) {
+        result += this._lineFeedHTML;
+      }
+      return result;
+    },
+
+    _addTabWrappers: function(content, html) {
+      // TODO(andybons): CSS tab-size is not supported in IE.
+      // Force this to be a number to prevent arbitrary injection.
+      var tabSize = +this.prefs.tab_size;
+      var htmlStr = '<span class="style-scope gr-diff-side tab ' +
+          (this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
+          'style="tab-size:' + tabSize + ';' +
+          '-moz-tab-size:' + tabSize + ';">\t</span>';
+      return html.replace(TAB_REGEX, htmlStr);
+    },
+
+    _createElement: function(tagName, className) {
+      var el = document.createElement(tagName);
+      // When Shady DOM is being used, these classes are added to account for
+      // Polymer's polyfill behavior. In order to guarantee sufficient
+      // specificity within the CSS rules, these are added to every element.
+      // Since the Polymer DOM utility functions (which would do this
+      // automatically) are not being used for performance reasons, this is
+      // done manually.
+      el.classList.add('style-scope', 'gr-diff-side');
+      if (!!className) {
+        el.classList.add(className);
+      }
+      return el;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-side-test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
similarity index 95%
rename from polygerrit-ui/app/test/gr-diff-side-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
index 95f4b78..85a1011 100644
--- a/polygerrit-ui/app/test/gr-diff-side-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-side</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-side.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-side.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
new file mode 100644
index 0000000..0dc18ae
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -0,0 +1,174 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../gr-diff/gr-diff.html">
+
+<dom-module id="gr-diff-view">
+  <template>
+    <style>
+      :host {
+        background-color: var(--view-background-color);
+        display: block;
+      }
+      h3 {
+        margin-top: 1em;
+        padding: .75em var(--default-horizontal-margin);
+      }
+      .reviewed {
+        display: inline-block;
+        margin: 0 .25em;
+        vertical-align: .15em;
+      }
+      .jumpToFileContainer {
+        display: inline-block;
+      }
+      .mobileJumpToFileContainer {
+        display: none;
+      }
+      .downArrow {
+        display: inline-block;
+        font-size: .6em;
+        vertical-align: middle;
+      }
+      .dropdown-trigger {
+        color: #00e;
+        cursor: pointer;
+        padding: 0;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+      }
+      .dropdown-content a {
+        cursor: pointer;
+        display: block;
+        font-weight: normal;
+        padding: .3em .5em;
+      }
+      .dropdown-content a:before {
+        color: #ccc;
+        content: attr(data-key-nav);
+        display: inline-block;
+        margin-right: .5em;
+        width: .3em;
+      }
+      .dropdown-content a:hover {
+        background-color: #00e;
+        color: #fff;
+      }
+      .dropdown-content a[selected] {
+        color: #000;
+        font-weight: bold;
+        pointer-events: none;
+        text-decoration: none;
+      }
+      .dropdown-content a[selected]:hover {
+        background-color: #fff;
+        color: #000;
+      }
+      gr-button {
+        font: inherit;
+        padding: .3em 0;
+        text-decoration: none;
+      }
+      @media screen and (max-width: 50em) {
+        .dash {
+          display: none;
+        }
+        .reviewed {
+          vertical-align: -.1em;
+        }
+        .jumpToFileContainer {
+          display: none;
+        }
+        .mobileJumpToFileContainer {
+          display: block;
+          width: 100%;
+        }
+      }
+    </style>
+    <gr-ajax id="changeDetailXHR"
+        auto
+        url="[[_computeChangeDetailPath(_changeNum)]]"
+        params="[[_computeChangeDetailQueryParams()]]"
+        last-response="{{_change}}"></gr-ajax>
+    <gr-ajax id="filesXHR"
+        auto
+        url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
+        on-response="_handleFilesResponse"></gr-ajax>
+    <gr-ajax id="configXHR"
+        auto
+        url="[[_computeProjectConfigPath(_change.project)]]"
+        last-response="{{_projectConfig}}"></gr-ajax>
+    <h3>
+      <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
+        [[_changeNum]]</a><span>:</span>
+      <span>[[_change.subject]]</span>
+      <span class="dash">—</span>
+      <input id="reviewed"
+          class="reviewed"
+          type="checkbox"
+          on-change="_handleReviewedChange"
+          hidden$="[[!_loggedIn]]" hidden>
+      <div class="jumpToFileContainer">
+        <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
+          <span>[[_computeFileDisplayName(_path)]]</span>
+          <span class="downArrow">&#9660;</span>
+        </gr-button>
+        <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
+          <div class="dropdown-content">
+            <template is="dom-repeat" items="[[_fileList]]" as="path">
+              <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
+                 selected$="[[_computeFileSelected(path, _path)]]"
+                 data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
+                 on-tap="_handleFileTap">
+                 [[_computeFileDisplayName(path)]]
+              </a>
+            </template>
+          </div>
+        </iron-dropdown>
+      </div>
+      <div class="mobileJumpToFileContainer">
+        <select on-change="_handleMobileSelectChange">
+          <template is="dom-repeat" items="[[_fileList]]" as="path">
+            <option
+                value$="[[path]]"
+                selected$="[[_computeFileSelected(path, _path)]]">
+              [[_computeFileDisplayName(path)]]
+            </option>
+          </template>
+        </select>
+      </div>
+    </h3>
+    <gr-diff id="diff"
+        change-num="[[_changeNum]]"
+        prefs="{{prefs}}"
+        patch-range="[[_patchRange]]"
+        path="[[_path]]"
+        project-config="[[_projectConfig]]"
+        available-patches="[[_computeAvailablePatches(_change.revisions)]]"
+        on-render="_handleDiffRender">
+    </gr-diff>
+  </template>
+  <script src="gr-diff-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
new file mode 100644
index 0000000..847a641
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -0,0 +1,315 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+
+  Polymer({
+    is: 'gr-diff-view',
+
+    /**
+     * Fired when the title of the page should change.
+     *
+     * @event title-change
+     */
+
+    properties: {
+      prefs: {
+        type: Object,
+        notify: true,
+      },
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+      changeViewState: {
+        type: Object,
+        notify: true,
+        value: function() { return {}; },
+      },
+
+      _patchRange: Object,
+      _change: Object,
+      _changeNum: String,
+      _diff: Object,
+      _fileList: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _path: {
+        type: String,
+        observer: '_pathChanged',
+      },
+      _loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+      _xhrPromise: Object,  // Used for testing.
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+      Gerrit.RESTClientBehavior,
+    ],
+
+    ready: function() {
+      app.accountReady.then(function() {
+        this._loggedIn = app.loggedIn;
+        if (this._loggedIn) {
+          this._setReviewed(true);
+        }
+      }.bind(this));
+    },
+
+    attached: function() {
+      if (this._path) {
+        this.fire('title-change',
+            {title: this._computeFileDisplayName(this._path)});
+      }
+      window.addEventListener('resize', this._boundWindowResizeHandler);
+    },
+
+    detached: function() {
+      window.removeEventListener('resize', this._boundWindowResizeHandler);
+    },
+
+    _handleReviewedChange: function(e) {
+      this._setReviewed(Polymer.dom(e).rootTarget.checked);
+    },
+
+    _setReviewed: function(reviewed) {
+      this.$.reviewed.checked = reviewed;
+      var method = reviewed ? 'PUT' : 'DELETE';
+      var url = this.changeBaseURL(this._changeNum,
+          this._patchRange.patchNum) + '/files/' +
+          encodeURIComponent(this._path) + '/reviewed';
+      this._send(method, url).catch(function(err) {
+        alert('Couldn’t change file review status. Check the console ' +
+            'and contact the PolyGerrit team for assistance.');
+        throw err;
+      }.bind(this));
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+
+      switch (e.keyCode) {
+        case 219:  // '['
+          e.preventDefault();
+          this._navToFile(this._fileList, -1);
+          break;
+        case 221:  // ']'
+          e.preventDefault();
+          this._navToFile(this._fileList, 1);
+          break;
+        case 78:  // 'n'
+          if (e.shiftKey) {
+            this.$.diff.scrollToNextCommentThread();
+          } else {
+            this.$.diff.scrollToNextDiffChunk();
+          }
+          break;
+        case 80:  // 'p'
+          if (e.shiftKey) {
+            this.$.diff.scrollToPreviousCommentThread();
+          } else {
+            this.$.diff.scrollToPreviousDiffChunk();
+          }
+          break;
+        case 65:  // 'a'
+          if (!this._loggedIn) { return; }
+
+          this.set('changeViewState.showReplyDialog', true);
+          /* falls through */ // required by JSHint
+        case 85:  // 'u'
+          if (this._changeNum && this._patchRange.patchNum) {
+            e.preventDefault();
+            page.show(this._computeChangePath(
+                this._changeNum,
+                this._patchRange.patchNum,
+                this._change && this._change.revisions));
+          }
+          break;
+        case 188:  // ','
+          this.$.diff.showDiffPreferences();
+          break;
+      }
+    },
+
+    _handleDiffRender: function() {
+      if (window.location.hash.length > 0) {
+        this.$.diff.scrollToLine(
+            parseInt(window.location.hash.substring(1), 10));
+      }
+    },
+
+    _navToFile: function(fileList, direction) {
+      if (fileList.length == 0) { return; }
+
+      var idx = fileList.indexOf(this._path) + direction;
+      if (idx < 0 || idx > fileList.length - 1) {
+        page.show(this._computeChangePath(
+            this._changeNum,
+            this._patchRange.patchNum,
+            this._change && this._change.revisions));
+        return;
+      }
+      page.show(this._computeDiffURL(this._changeNum,
+                                     this._patchRange,
+                                     fileList[idx]));
+    },
+
+    _paramsChanged: function(value) {
+      if (value.view != this.tagName.toLowerCase()) { return; }
+
+      this._changeNum = value.changeNum;
+      this._patchRange = {
+        patchNum: value.patchNum,
+        basePatchNum: value.basePatchNum || 'PARENT',
+      };
+      this._path = value.path;
+
+      this.fire('title-change',
+          {title: this._computeFileDisplayName(this._path)});
+
+      // When navigating away from the page, there is a possibility that the
+      // patch number is no longer a part of the URL (say when navigating to
+      // the top-level change info view) and therefore undefined in `params`.
+      if (!this._patchRange.patchNum) {
+        return;
+      }
+
+      this.$.diff.reload();
+    },
+
+    _pathChanged: function(path) {
+      if (this._fileList.length == 0) { return; }
+
+      this.set('changeViewState.selectedFileIndex',
+          this._fileList.indexOf(path));
+
+      if (this._loggedIn) {
+        this._setReviewed(true);
+      }
+    },
+
+    _computeDiffURL: function(changeNum, patchRange, path) {
+      var patchStr = patchRange.patchNum;
+      if (patchRange.basePatchNum != null &&
+          patchRange.basePatchNum != 'PARENT') {
+        patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
+      }
+      return '/c/' + changeNum + '/' + patchStr + '/' + path;
+    },
+
+    _computeAvailablePatches: function(revisions) {
+      var patchNums = [];
+      for (var rev in revisions) {
+        patchNums.push(revisions[rev]._number);
+      }
+      return patchNums.sort(function(a, b) { return a - b; });
+    },
+
+    _computeChangePath: function(changeNum, patchNum, revisions) {
+      var base = '/c/' + changeNum + '/';
+
+      // The change may not have loaded yet, making revisions unavailable.
+      if (!revisions) {
+        return base + patchNum;
+      }
+
+      var latestPatchNum = -1;
+      for (var rev in revisions) {
+        latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
+      }
+      if (parseInt(patchNum, 10) != latestPatchNum) {
+        return base + patchNum;
+      }
+
+      return base;
+    },
+
+    _computeFileDisplayName: function(path) {
+      return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+    },
+
+    _computeChangeDetailPath: function(changeNum) {
+      return '/changes/' + changeNum + '/detail';
+    },
+
+    _computeChangeDetailQueryParams: function() {
+      return {O: this.listChangesOptionsToHex(
+          this.ListChangesOption.ALL_REVISIONS
+      )};
+    },
+
+    _computeFilesPath: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/files';
+    },
+
+    _computeProjectConfigPath: function(project) {
+      return '/projects/' + encodeURIComponent(project) + '/config';
+    },
+
+    _computeFileSelected: function(path, currentPath) {
+      return path == currentPath;
+    },
+
+    _computeKeyNav: function(path, selectedPath, fileList) {
+      var selectedIndex = fileList.indexOf(selectedPath);
+      if (fileList.indexOf(path) == selectedIndex - 1) {
+        return '[';
+      }
+      if (fileList.indexOf(path) == selectedIndex + 1) {
+        return ']';
+      }
+      return '';
+    },
+
+    _handleFileTap: function(e) {
+      this.$.dropdown.close();
+    },
+
+    _handleMobileSelectChange: function(e) {
+      var path = Polymer.dom(e).rootTarget.value;
+      page.show(
+          this._computeDiffURL(this._changeNum, this._patchRange, path));
+    },
+
+    _handleFilesResponse: function(e, req) {
+      this._fileList = Object.keys(e.detail.response).sort();
+    },
+
+    _showDropdownTapHandler: function(e) {
+      this.$.dropdown.open();
+    },
+
+    _send: function(method, url) {
+      var xhr = document.createElement('gr-request');
+      this._xhrPromise = xhr.send({
+        method: method,
+        url: url,
+      });
+      return this._xhrPromise;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-view-test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
similarity index 96%
rename from polygerrit-ui/app/test/gr-diff-view-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index da3626c..bfe4906 100644
--- a/polygerrit-ui/app/test/gr-diff-view-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -18,14 +18,14 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-view</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-view.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-view.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
new file mode 100644
index 0000000..21ee076
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -0,0 +1,123 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
+<link rel="import" href="../gr-diff-side/gr-diff-side.html">
+<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
+
+<dom-module id="gr-diff">
+  <template>
+    <style>
+      .loading {
+        padding: 0 var(--default-horizontal-margin) 1em;
+        color: #666;
+      }
+      .header {
+        display: flex;
+        justify-content: space-between;
+        margin: 0 var(--default-horizontal-margin) .75em;
+      }
+      .prefsButton {
+        text-align: right;
+      }
+      .diffContainer {
+        border-bottom: 1px solid #eee;
+        border-top: 1px solid #eee;
+        display: flex;
+        font: 12px var(--monospace-font-family);
+        overflow-x: auto;
+      }
+      gr-diff-side:first-of-type {
+        --light-highlight-color: #fee;
+        --dark-highlight-color: #ffd4d4;
+      }
+      gr-diff-side:last-of-type {
+        --light-highlight-color: #efe;
+        --dark-highlight-color: #d4ffd4;
+        border-right: 1px solid #ddd;
+      }
+    </style>
+    <gr-ajax id="diffXHR"
+        url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
+        params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
+        last-response="{{_diffResponse}}"
+        loading="{{_loading}}"></gr-ajax>
+    <gr-ajax id="baseCommentsXHR"
+        url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+    <gr-ajax id="commentsXHR"
+        url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+    <gr-ajax id="baseDraftsXHR"
+        url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+    <gr-ajax id="draftsXHR"
+        url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+    <div hidden$="[[_loading]]" hidden>
+      <div class="header">
+        <gr-patch-range-select
+            path="[[path]]"
+            change-num="[[changeNum]]"
+            patch-range="[[patchRange]]"
+            available-patches="[[availablePatches]]"></gr-patch-range-select>
+        <gr-button link
+           class="prefsButton"
+           on-tap="_handlePrefsTap"
+           hidden$="[[!prefs]]"
+           hidden>Diff View Preferences</gr-button>
+      </div>
+      <gr-overlay id="prefsOverlay" with-backdrop>
+        <gr-diff-preferences
+            prefs="{{prefs}}"
+            on-save="_handlePrefsSave"
+            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
+      </gr-overlay>
+
+      <div class="diffContainer">
+        <gr-diff-side id="leftDiff"
+            change-num="[[changeNum]]"
+            patch-num="[[patchRange.basePatchNum]]"
+            path="[[path]]"
+            content="{{_diff.leftSide}}"
+            prefs="[[prefs]]"
+            can-comment="[[_loggedIn]]"
+            project-config="[[projectConfig]]"
+            on-expand-context="_handleExpandContext"
+            on-thread-height-change="_handleThreadHeightChange"
+            on-add-draft="_handleAddDraft"
+            on-remove-thread="_handleRemoveThread"></gr-diff-side>
+        <gr-diff-side id="rightDiff"
+            change-num="[[changeNum]]"
+            patch-num="[[patchRange.patchNum]]"
+            path="[[path]]"
+            content="{{_diff.rightSide}}"
+            prefs="[[prefs]]"
+            can-comment="[[_loggedIn]]"
+            project-config="[[projectConfig]]"
+            on-expand-context="_handleExpandContext"
+            on-thread-height-change="_handleThreadHeightChange"
+            on-add-draft="_handleAddDraft"
+            on-remove-thread="_handleRemoveThread"></gr-diff-side>
+      </div>
+    </div>
+  </template>
+  <script src="gr-diff.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
new file mode 100644
index 0000000..485e2cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -0,0 +1,746 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-diff',
+
+    /**
+     * Fired when the diff is rendered.
+     *
+     * @event render
+     */
+
+    properties: {
+      availablePatches: Array,
+      changeNum: String,
+      /*
+       * A single object to encompass basePatchNum and patchNum is used
+       * so that both can be set at once without incremental observers
+       * firing after each property changes.
+       */
+      patchRange: Object,
+      path: String,
+      prefs: {
+        type: Object,
+        notify: true,
+      },
+      projectConfig: Object,
+
+      _prefsReady: {
+        type: Object,
+        readOnly: true,
+        value: function() {
+          return new Promise(function(resolve) {
+            this._resolvePrefsReady = resolve;
+          }.bind(this));
+        },
+      },
+      _baseComments: Array,
+      _comments: Array,
+      _drafts: Array,
+      _baseDrafts: Array,
+      /**
+       * Base (left side) comments and drafts grouped by line number.
+       * Only used for initial rendering.
+       */
+      _groupedBaseComments: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      /**
+       * Comments and drafts (right side) grouped by line number.
+       * Only used for initial rendering.
+       */
+      _groupedComments: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      _diffResponse: Object,
+      _diff: {
+        type: Object,
+        value: function() { return {}; },
+      },
+      _loggedIn: {
+        type: Boolean,
+        value: false,
+      },
+      _initialRenderComplete: {
+        type: Boolean,
+        value: false,
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _savedPrefs: Object,
+
+      _diffRequestsPromise: Object,  // Used for testing.
+      _diffPreferencesPromise: Object,  // Used for testing.
+    },
+
+    behaviors: [
+      Gerrit.RESTClientBehavior,
+    ],
+
+    observers: [
+      '_prefsChanged(prefs.*)',
+    ],
+
+    ready: function() {
+      app.accountReady.then(function() {
+        this._loggedIn = app.loggedIn;
+      }.bind(this));
+    },
+
+    scrollToLine: function(lineNum) {
+      // TODO(andybons): Should this always be the right side?
+      this.$.rightDiff.scrollToLine(lineNum);
+    },
+
+    scrollToNextDiffChunk: function() {
+      this.$.rightDiff.scrollToNextDiffChunk();
+    },
+
+    scrollToPreviousDiffChunk: function() {
+      this.$.rightDiff.scrollToPreviousDiffChunk();
+    },
+
+    scrollToNextCommentThread: function() {
+      this.$.rightDiff.scrollToNextCommentThread();
+    },
+
+    scrollToPreviousCommentThread: function() {
+      this.$.rightDiff.scrollToPreviousCommentThread();
+    },
+
+    reload: function(changeNum, patchRange, path) {
+      // If a diff takes a considerable amount of time to render, the previous
+      // diff can end up showing up while the DOM is constructed. Clear the
+      // content on a reload to prevent this.
+      this._diff = {
+        leftSide: [],
+        rightSide: [],
+      };
+
+      var promises = [
+        this._prefsReady,
+        this.$.diffXHR.generateRequest().completes
+      ];
+
+      var basePatchNum = this.patchRange.basePatchNum;
+
+      return app.accountReady.then(function() {
+        promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
+        this._diffRequestsPromise = Promise.all(promises).then(function() {
+          this._render();
+        }.bind(this)).catch(function(err) {
+          alert('Oops. Something went wrong. Check the console and bug the ' +
+              'PolyGerrit team for assistance.');
+          throw err;
+        });
+      }.bind(this));
+    },
+
+    showDiffPreferences: function() {
+      this.$.prefsOverlay.open();
+    },
+
+    _prefsChanged: function(changeRecord) {
+      if (this._initialRenderComplete) {
+        this._render();
+      }
+      this._resolvePrefsReady(changeRecord.base);
+    },
+
+    _render: function() {
+      this._groupCommentsAndDrafts();
+      this._processContent();
+
+      // Allow for the initial rendering to complete before firing the event.
+      this.async(function() {
+        this.fire('render', null, {bubbles: false});
+      }.bind(this), 1);
+
+      this._initialRenderComplete = true;
+    },
+
+    _getCommentsAndDrafts: function(basePatchNum, loggedIn) {
+      function onlyParent(c) { return c.side == 'PARENT'; }
+      function withoutParent(c) { return c.side != 'PARENT'; }
+
+      var promises = [];
+      var commentsPromise = this.$.commentsXHR.generateRequest().completes;
+      promises.push(commentsPromise.then(function(req) {
+        var comments = req.response[this.path] || [];
+        if (basePatchNum == 'PARENT') {
+          this._baseComments = comments.filter(onlyParent);
+        }
+        this._comments = comments.filter(withoutParent);
+      }.bind(this)));
+
+      if (basePatchNum != 'PARENT') {
+        commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
+        promises.push(commentsPromise.then(function(req) {
+          this._baseComments =
+            (req.response[this.path] || []).filter(withoutParent);
+        }.bind(this)));
+      }
+
+      if (!loggedIn) {
+        this._baseDrafts = [];
+        this._drafts = [];
+        return Promise.all(promises);
+      }
+
+      var draftsPromise = this.$.draftsXHR.generateRequest().completes;
+      promises.push(draftsPromise.then(function(req) {
+        var drafts = req.response[this.path] || [];
+        if (basePatchNum == 'PARENT') {
+          this._baseDrafts = drafts.filter(onlyParent);
+        }
+        this._drafts = drafts.filter(withoutParent);
+      }.bind(this)));
+
+      if (basePatchNum != 'PARENT') {
+        draftsPromise = this.$.baseDraftsXHR.generateRequest().completes;
+        promises.push(draftsPromise.then(function(req) {
+          this._baseDrafts =
+              (req.response[this.path] || []).filter(withoutParent);
+        }.bind(this)));
+      }
+
+      return Promise.all(promises);
+    },
+
+    _computeDiffPath: function(changeNum, patchNum, path) {
+      return this.changeBaseURL(changeNum, patchNum) + '/files/' +
+          encodeURIComponent(path) + '/diff';
+    },
+
+    _computeCommentsPath: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/comments';
+    },
+
+    _computeDraftsPath: function(changeNum, patchNum) {
+      return this.changeBaseURL(changeNum, patchNum) + '/drafts';
+    },
+
+    _computeDiffQueryParams: function(basePatchNum) {
+      var params =  {
+        context: 'ALL',
+        intraline: null
+      };
+      if (basePatchNum != 'PARENT') {
+        params.base = basePatchNum;
+      }
+      return params;
+    },
+
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+
+      // TODO(andybons): This is not supported in IE. Implement a polyfill.
+      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
+      // an object as a value, it must be marked enumerable.
+      this._savedPrefs = Object.assign({}, this.prefs);
+      this.$.prefsOverlay.open();
+    },
+
+    _handlePrefsSave: function(e) {
+      e.stopPropagation();
+      var el = Polymer.dom(e).rootTarget;
+      el.disabled = true;
+      app.accountReady.then(function() {
+        if (!this._loggedIn) {
+          el.disabled = false;
+          this.$.prefsOverlay.close();
+          return;
+        }
+        this._saveDiffPreferences().then(function() {
+          this.$.prefsOverlay.close();
+          el.disabled = false;
+        }.bind(this)).catch(function(err) {
+          el.disabled = false;
+          alert('Oops. Something went wrong. Check the console and bug the ' +
+                'PolyGerrit team for assistance.');
+          throw err;
+        });
+      }.bind(this));
+    },
+
+    _saveDiffPreferences: function() {
+      var xhr = document.createElement('gr-request');
+      this._diffPreferencesPromise = xhr.send({
+        method: 'PUT',
+        url: '/accounts/self/preferences.diff',
+        body: this.prefs,
+      });
+      return this._diffPreferencesPromise;
+    },
+
+    _handlePrefsCancel: function(e) {
+      e.stopPropagation();
+      this.prefs = this._savedPrefs;
+      this.$.prefsOverlay.close();
+    },
+
+    _handleExpandContext: function(e) {
+      var ctx = e.detail.context;
+      var contextControlIndex = -1;
+      for (var i = ctx.start; i <= ctx.end; i++) {
+        this._diff.leftSide[i].hidden = false;
+        this._diff.rightSide[i].hidden = false;
+        if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
+            this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
+          contextControlIndex = i;
+        }
+      }
+      this._diff.leftSide[contextControlIndex].hidden = true;
+      this._diff.rightSide[contextControlIndex].hidden = true;
+
+      this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
+      this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
+
+      this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
+      this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
+    },
+
+    _handleThreadHeightChange: function(e) {
+      var index = e.detail.index;
+      var diffEl = Polymer.dom(e).rootTarget;
+      var otherSide = diffEl == this.$.leftDiff ?
+          this.$.rightDiff : this.$.leftDiff;
+
+      var threadHeight = e.detail.height;
+      var otherSideHeight;
+      if (otherSide.content[index].type == 'COMMENT_THREAD') {
+        otherSideHeight = otherSide.getRowNaturalHeight(index);
+      } else {
+        otherSideHeight = otherSide.getRowHeight(index);
+      }
+      var maxHeight = Math.max(threadHeight, otherSideHeight);
+      this.$.leftDiff.setRowHeight(index, maxHeight);
+      this.$.rightDiff.setRowHeight(index, maxHeight);
+    },
+
+    _handleAddDraft: function(e) {
+      var insertIndex = e.detail.index + 1;
+      var diffEl = Polymer.dom(e).rootTarget;
+      var content = diffEl.content;
+      if (content[insertIndex] &&
+          content[insertIndex].type == 'COMMENT_THREAD') {
+        // A thread is already here. Do nothing.
+        return;
+      }
+      var comment = {
+        type: 'COMMENT_THREAD',
+        comments: [{
+          __draft: true,
+          __draftID: Math.random().toString(36),
+          line: e.detail.line,
+          path: this.path,
+        }]
+      };
+      if (diffEl == this.$.leftDiff &&
+          this.patchRange.basePatchNum == 'PARENT') {
+        comment.comments[0].side = 'PARENT';
+        comment.patchNum = this.patchRange.patchNum;
+      }
+
+      if (content[insertIndex] &&
+          content[insertIndex].type == 'FILLER') {
+        content[insertIndex] = comment;
+        diffEl.rowUpdated(insertIndex);
+      } else {
+        content.splice(insertIndex, 0, comment);
+        diffEl.rowInserted(insertIndex);
+      }
+
+      var otherSide = diffEl == this.$.leftDiff ?
+          this.$.rightDiff : this.$.leftDiff;
+      if (otherSide.content[insertIndex] == null ||
+          otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
+        otherSide.content.splice(insertIndex, 0, {
+          type: 'FILLER',
+        });
+        otherSide.rowInserted(insertIndex);
+      }
+    },
+
+    _handleRemoveThread: function(e) {
+      var diffEl = Polymer.dom(e).rootTarget;
+      var otherSide = diffEl == this.$.leftDiff ?
+          this.$.rightDiff : this.$.leftDiff;
+      var index = e.detail.index;
+
+      if (otherSide.content[index].type == 'FILLER') {
+        otherSide.content.splice(index, 1);
+        otherSide.rowRemoved(index);
+        diffEl.content.splice(index, 1);
+        diffEl.rowRemoved(index);
+      } else if (otherSide.content[index].type == 'COMMENT_THREAD') {
+        diffEl.content[index] = {type: 'FILLER'};
+        diffEl.rowUpdated(index);
+        var height = otherSide.setRowNaturalHeight(index);
+        diffEl.setRowHeight(index, height);
+      } else {
+        throw Error('A thread cannot be opposite anything but filler or ' +
+            'another thread');
+      }
+    },
+
+    _processContent: function() {
+      var leftSide = [];
+      var rightSide = [];
+      var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
+      var ctx = {
+        hidingLines: false,
+        lastNumLinesHidden: 0,
+        left: {
+          lineNum: initialLineNum,
+        },
+        right: {
+          lineNum: initialLineNum,
+        }
+      };
+      var content = this._breakUpCommonChunksWithComments(ctx,
+          this._diffResponse.content);
+      var context = this.prefs.context;
+      if (context == -1) {
+        // Show the entire file.
+        context = Infinity;
+      }
+      for (var i = 0; i < content.length; i++) {
+        if (i == 0) {
+          ctx.skipRange = [0, context];
+        } else if (i == content.length - 1) {
+          ctx.skipRange = [context, 0];
+        } else {
+          ctx.skipRange = [context, context];
+        }
+        ctx.diffChunkIndex = i;
+        this._addDiffChunk(ctx, content[i], leftSide, rightSide);
+      }
+
+      this._diff = {
+        leftSide: leftSide,
+        rightSide: rightSide,
+      };
+    },
+
+    // In order to show comments out of the bounds of the selected context,
+    // treat them as diffs within the model so that the content (and context
+    // surrounding it) renders correctly.
+    _breakUpCommonChunksWithComments: function(ctx, content) {
+      var result = [];
+      var leftLineNum = ctx.left.lineNum;
+      var rightLineNum = ctx.right.lineNum;
+      for (var i = 0; i < content.length; i++) {
+        if (!content[i].ab) {
+          result.push(content[i]);
+          if (content[i].a) {
+            leftLineNum += content[i].a.length;
+          }
+          if (content[i].b) {
+            rightLineNum += content[i].b.length;
+          }
+          continue;
+        }
+        var chunk = content[i].ab;
+        var currentChunk = {ab: []};
+        for (var j = 0; j < chunk.length; j++) {
+          leftLineNum++;
+          rightLineNum++;
+          if (this._groupedBaseComments[leftLineNum] == null &&
+              this._groupedComments[rightLineNum] == null) {
+            currentChunk.ab.push(chunk[j]);
+          } else {
+            if (currentChunk.ab && currentChunk.ab.length > 0) {
+              result.push(currentChunk);
+              currentChunk = {ab: []};
+            }
+            // Append an annotation to indicate that this line should not be
+            // highlighted even though it's implied with both `a` and `b`
+            // defined. This is needed since there may be two lines that
+            // should be highlighted but are equal (blank lines, for example).
+            result.push({
+              __noHighlight: true,
+              a: [chunk[j]],
+              b: [chunk[j]],
+            });
+          }
+        }
+        if (currentChunk.ab != null && currentChunk.ab.length > 0) {
+          result.push(currentChunk);
+        }
+      }
+      return result;
+    },
+
+    _groupCommentsAndDrafts: function() {
+      this._baseDrafts.forEach(function(d) { d.__draft = true; });
+      this._drafts.forEach(function(d) { d.__draft = true; });
+      var allLeft = this._baseComments.concat(this._baseDrafts);
+      var allRight = this._comments.concat(this._drafts);
+
+      var leftByLine = {};
+      var rightByLine = {};
+      var mapFunc = function(byLine) {
+        return function(c) {
+          // File comments/drafts are grouped with line 1 for now.
+          var line = c.line || 1;
+          if (byLine[line] == null) {
+            byLine[line] = [];
+          }
+          byLine[line].push(c);
+        };
+      };
+      allLeft.forEach(mapFunc(leftByLine));
+      allRight.forEach(mapFunc(rightByLine));
+
+      this._groupedBaseComments = leftByLine;
+      this._groupedComments = rightByLine;
+    },
+
+    _addContextControl: function(ctx, leftSide, rightSide) {
+      var numLinesHidden = ctx.lastNumLinesHidden;
+      var leftStart = leftSide.length - numLinesHidden;
+      var leftEnd = leftSide.length;
+      var rightStart = rightSide.length - numLinesHidden;
+      var rightEnd = rightSide.length;
+      if (leftStart != rightStart || leftEnd != rightEnd) {
+        throw Error(
+            'Left and right ranges for context control should be equal:' +
+            'Left: [' + leftStart + ', ' + leftEnd + '] ' +
+            'Right: [' + rightStart + ', ' + rightEnd + ']');
+      }
+      var obj = {
+        type: 'CONTEXT_CONTROL',
+        numLines: numLinesHidden,
+        start: leftStart,
+        end: leftEnd,
+      };
+      // NOTE: Be careful, here. This object is meant to be immutable. If the
+      // object is altered within one side's array it will reflect the
+      // alterations in another.
+      leftSide.push(obj);
+      rightSide.push(obj);
+    },
+
+    _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
+      for (var i = 0; i < chunk.ab.length; i++) {
+        var numLines = Math.ceil(
+            this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
+        var hidden = i >= ctx.skipRange[0] &&
+            i < chunk.ab.length - ctx.skipRange[1];
+        if (ctx.hidingLines && hidden == false) {
+          // No longer hiding lines. Add a context control.
+          this._addContextControl(ctx, leftSide, rightSide);
+          ctx.lastNumLinesHidden = 0;
+        }
+        ctx.hidingLines = hidden;
+        if (hidden) {
+          ctx.lastNumLinesHidden++;
+        }
+
+        // Blank lines within a diff content array indicate a newline.
+        leftSide.push({
+          type: 'CODE',
+          hidden: hidden,
+          content: chunk.ab[i] || '\n',
+          numLines: numLines,
+          lineNum: ++ctx.left.lineNum,
+        });
+        rightSide.push({
+          type: 'CODE',
+          hidden: hidden,
+          content: chunk.ab[i] || '\n',
+          numLines: numLines,
+          lineNum: ++ctx.right.lineNum,
+        });
+
+        this._addCommentsIfPresent(ctx, leftSide, rightSide);
+      }
+      if (ctx.lastNumLinesHidden > 0) {
+        this._addContextControl(ctx, leftSide, rightSide);
+      }
+    },
+
+    _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
+      if (chunk.ab) {
+        this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
+        return;
+      }
+
+      var leftHighlights = [];
+      if (chunk.edit_a) {
+        leftHighlights =
+            this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
+      }
+      var rightHighlights = [];
+      if (chunk.edit_b) {
+        rightHighlights =
+            this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
+      }
+
+      var aLen = (chunk.a && chunk.a.length) || 0;
+      var bLen = (chunk.b && chunk.b.length) || 0;
+      var maxLen = Math.max(aLen, bLen);
+      for (var i = 0; i < maxLen; i++) {
+        var hasLeftContent = chunk.a && i < chunk.a.length;
+        var hasRightContent = chunk.b && i < chunk.b.length;
+        var leftContent = hasLeftContent ? chunk.a[i] : '';
+        var rightContent = hasRightContent ? chunk.b[i] : '';
+        var highlight = !chunk.__noHighlight;
+        var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
+        if (hasLeftContent) {
+          leftSide.push({
+            type: 'CODE',
+            content: leftContent || '\n',
+            numLines: maxNumLines,
+            lineNum: ++ctx.left.lineNum,
+            highlight: highlight,
+            intraline: highlight && leftHighlights.filter(function(hl) {
+              return hl.contentIndex == i;
+            }),
+          });
+        } else {
+          leftSide.push({
+            type: 'FILLER',
+            numLines: maxNumLines,
+          });
+        }
+        if (hasRightContent) {
+          rightSide.push({
+            type: 'CODE',
+            content: rightContent || '\n',
+            numLines: maxNumLines,
+            lineNum: ++ctx.right.lineNum,
+            highlight: highlight,
+            intraline: highlight && rightHighlights.filter(function(hl) {
+              return hl.contentIndex == i;
+            }),
+          });
+        } else {
+          rightSide.push({
+            type: 'FILLER',
+            numLines: maxNumLines,
+          });
+        }
+        this._addCommentsIfPresent(ctx, leftSide, rightSide);
+      }
+    },
+
+    _addCommentsIfPresent: function(ctx, leftSide, rightSide) {
+      var leftComments = this._groupedBaseComments[ctx.left.lineNum];
+      var rightComments = this._groupedComments[ctx.right.lineNum];
+      if (leftComments) {
+        var thread = {
+          type: 'COMMENT_THREAD',
+          comments: leftComments,
+        };
+        if (this.patchRange.basePatchNum == 'PARENT') {
+          thread.patchNum = this.patchRange.patchNum;
+        }
+        leftSide.push(thread);
+      }
+      if (rightComments) {
+        rightSide.push({
+          type: 'COMMENT_THREAD',
+          comments: rightComments,
+        });
+      }
+      if (leftComments && !rightComments) {
+        rightSide.push({type: 'FILLER'});
+      } else if (!leftComments && rightComments) {
+        leftSide.push({type: 'FILLER'});
+      }
+      this._groupedBaseComments[ctx.left.lineNum] = null;
+      this._groupedComments[ctx.right.lineNum] = null;
+    },
+
+    // The `highlights` array consists of a list of <skip length, mark length>
+    // pairs, where the skip length is the number of characters between the
+    // end of the previous edit and the start of this edit, and the mark
+    // length is the number of edited characters following the skip. The start
+    // of the edits is from the beginning of the related diff content lines.
+    //
+    // Note that the implied newline character at the end of each line is
+    // included in the length calculation, and thus it is possible for the
+    // edits to span newlines.
+    //
+    // A line highlight object consists of three fields:
+    // - contentIndex: The index of the diffChunk `content` field (the line
+    //   being referred to).
+    // - startIndex: Where the highlight should begin.
+    // - endIndex: (optional) Where the highlight should end. If omitted, the
+    //   highlight is meant to be a continuation onto the next line.
+    _normalizeIntralineHighlights: function(content, highlights) {
+      var contentIndex = 0;
+      var idx = 0;
+      var normalized = [];
+      for (var i = 0; i < highlights.length; i++) {
+        var line = content[contentIndex] + '\n';
+        var hl = highlights[i];
+        var j = 0;
+        while (j < hl[0]) {
+          if (idx == line.length) {
+            idx = 0;
+            line = content[++contentIndex] + '\n';
+            continue;
+          }
+          idx++;
+          j++;
+        }
+        var lineHighlight = {
+          contentIndex: contentIndex,
+          startIndex: idx,
+        };
+
+        j = 0;
+        while (line && j < hl[1]) {
+          if (idx == line.length) {
+            idx = 0;
+            line = content[++contentIndex] + '\n';
+            normalized.push(lineHighlight);
+            lineHighlight = {
+              contentIndex: contentIndex,
+              startIndex: idx,
+            };
+            continue;
+          }
+          idx++;
+          j++;
+        }
+        lineHighlight.endIndex = idx;
+        normalized.push(lineHighlight);
+      }
+      return normalized;
+    },
+
+    _visibleLineLength: function(contents) {
+      // http://jsperf.com/performance-of-match-vs-split
+      var numTabs = contents.split('\t').length - 1;
+      return contents.length - numTabs + (this.prefs.tab_size * numTabs);
+    },
+
+    _maxLinesSpanned: function(left, right) {
+      return Math.max(
+          Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
+          Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-diff-test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
similarity index 97%
rename from polygerrit-ui/app/test/gr-diff-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index cd0f158..9a8cb81 100644
--- a/polygerrit-ui/app/test/gr-diff-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
new file mode 100644
index 0000000..b0ee0b73
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -0,0 +1,53 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-patch-range-select">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      .patchRange {
+        display: inline-block;
+      }
+    </style>
+    Patch set:
+    <span class="patchRange">
+      <select id="leftPatchSelect" on-change="_handlePatchChange">
+        <option value="PARENT"
+            selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
+        <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+          <option value$="[[patchNum]]"
+              selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
+              disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+        </template>
+      </select>
+    </span>
+    &rarr;
+    <span class="patchRange">
+      <select id="rightPatchSelect" on-change="_handlePatchChange">
+        <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+          <option value$="[[patchNum]]"
+              selected$="[[_computeRightSelected(patchNum, patchRange)]]"
+              disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+        </template>
+      </select>
+    </span>
+  </template>
+  <script src="gr-patch-range-select.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
new file mode 100644
index 0000000..3439ecd
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-patch-range-select',
+
+    properties: {
+      availablePatches: Array,
+      changeNum: String,
+      patchRange: Object,
+      path: String,
+    },
+
+    _handlePatchChange: function(e) {
+      var leftPatch = this.$.leftPatchSelect.value;
+      var rightPatch = this.$.rightPatchSelect.value;
+      var rangeStr = rightPatch;
+      if (leftPatch != 'PARENT') {
+        rangeStr = leftPatch + '..' + rangeStr;
+      }
+      page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
+    },
+
+    _computeLeftSelected: function(patchNum, patchRange) {
+      return patchNum == patchRange.basePatchNum;
+    },
+
+    _computeRightSelected: function(patchNum, patchRange) {
+      return patchNum == patchRange.patchNum;
+    },
+
+    _computeLeftDisabled: function(patchNum, patchRange) {
+      return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
+    },
+
+    _computeRightDisabled: function(patchNum, patchRange) {
+      if (patchRange.basePatchNum == 'PARENT') { return false; }
+      return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-patch-range-select-test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
similarity index 88%
rename from polygerrit-ui/app/test/gr-patch-range-select-test.html
rename to polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 1e80511..a7d909e 100644
--- a/polygerrit-ui/app/test/gr-patch-range-select-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-patch-range-select</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-patch-range-select.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-patch-range-select.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-ajax.html b/polygerrit-ui/app/elements/gr-ajax.html
deleted file mode 100644
index 58b647e..0000000
--- a/polygerrit-ui/app/elements/gr-ajax.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
-
-<dom-module id="gr-ajax">
-  <template>
-    <iron-ajax id="xhr"
-        auto="[[auto]]"
-        url="[[url]]"
-        params="[[params]]"
-        json-prefix=")]}'"
-        last-error="{{lastError}}"
-        last-response="{{lastResponse}}"
-        loading="{{loading}}"
-        on-response="_handleResponse"
-        on-error="_handleError"
-        debounce-duration="300"></iron-ajax>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-ajax',
-
-      /**
-       * Fired when a response is received.
-       *
-       * @event response
-       */
-
-      /**
-       * Fired when an error is received.
-       *
-       * @event error
-       */
-
-      hostAttributes: {
-        hidden: true
-      },
-
-      properties: {
-        auto: {
-          type: Boolean,
-          value: false,
-        },
-        url: String,
-        params: {
-          type: Object,
-          value: function() {
-            return {};
-          },
-        },
-        lastError: {
-          type: Object,
-          notify: true,
-        },
-        lastResponse: {
-          type: Object,
-          notify: true,
-        },
-        loading: {
-          type: Boolean,
-          notify: true,
-        },
-      },
-
-      ready: function() {
-        // Used for debugging which element a request came from.
-        var headers = this.$.xhr.headers;
-        headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)';
-        this.$.xhr.headers = headers;
-      },
-
-      generateRequest: function() {
-        return this.$.xhr.generateRequest();
-      },
-
-      _handleResponse: function(e, req) {
-        this.fire('response', req, {bubbles: false});
-      },
-
-      _handleError: function(e, req) {
-        this.fire('error', req, {bubbles: false});
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 8890983..e7fa31c 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -17,15 +17,17 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../styles/app-theme.html">
-<link rel="import" href="gr-account-dropdown.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-change-list-view.html">
-<link rel="import" href="gr-change-view.html">
-<link rel="import" href="gr-dashboard-view.html">
-<link rel="import" href="gr-diff-view.html">
-<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
-<link rel="import" href="gr-overlay.html">
-<link rel="import" href="gr-search-bar.html">
+
+<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
+<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
+<link rel="import" href="./change/gr-change-view/gr-change-view.html">
+<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+
+<link rel="import" href="./shared/gr-account-dropdown/gr-account-dropdown.html">
+<link rel="import" href="./shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="./shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
+<link rel="import" href="./shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="./shared/gr-search-bar/gr-search-bar.html">
 
 <script src="../bower_components/page/page.js"></script>
 <script src="../scripts/app.js"></script>
@@ -166,164 +168,5 @@
           on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
     </gr-overlay>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-app',
-
-      properties: {
-        account: {
-          type: Object,
-          observer: '_accountChanged',
-        },
-        accountReady: {
-          type: Object,
-          readOnly: true,
-          notify: true,
-          value: function() {
-            return new Promise(function(resolve) {
-              this._resolveAccountReady = resolve;
-            }.bind(this));
-          },
-        },
-        config: {
-          type: Object,
-          observer: '_configChanged',
-        },
-        configReady: {
-          type: Object,
-          readOnly: true,
-          notify: true,
-          value: function() {
-            return new Promise(function(resolve) {
-              this._resolveConfigReady = resolve;
-            }.bind(this));
-          },
-        },
-        version: String,
-        params: Object,
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-
-        _diffPreferences: Object,
-        _showChangeListView: Boolean,
-        _showDashboardView: Boolean,
-        _showChangeView: Boolean,
-        _showDiffView: Boolean,
-        _viewState: Object,
-      },
-
-      listeners: {
-        'title-change': '_handleTitleChange',
-      },
-
-      observers: [
-        '_viewChanged(params.view)',
-      ],
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-      ],
-
-      get loggedIn() {
-        return !!(this.account && Object.keys(this.account).length > 0);
-      },
-
-      ready: function() {
-        this._viewState = {
-          changeView: {
-            changeNum: null,
-            patchNum: null,
-            selectedFileIndex: 0,
-            showReplyDialog: false,
-          },
-          changeListView: {
-            query: null,
-            offset: 0,
-            selectedChangeIndex: 0,
-          },
-          dashboardView: {
-            selectedChangeIndex: 0,
-          },
-        };
-      },
-
-      _accountChanged: function() {
-        this._resolveAccountReady();
-        this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
-        this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
-        if (this.loggedIn) {
-          this.$.diffPreferencesXHR.generateRequest();
-        } else {
-          // These defaults should match the defaults in
-          // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
-          // NOTE: There are some settings that don't apply to PolyGerrit
-          // (Render mode being at least one of them).
-          this._diffPreferences = {
-            auto_hide_diff_table_header: true,
-            context: 10,
-            cursor_blink_rate: 0,
-            ignore_whitespace: 'IGNORE_NONE',
-            intraline_difference: true,
-            line_length: 100,
-            show_line_endings: true,
-            show_tabs: true,
-            show_whitespace_errors: true,
-            syntax_highlighting: true,
-            tab_size: 8,
-            theme: 'DEFAULT',
-          };
-        }
-      },
-
-      _configChanged: function(config) {
-        this._resolveConfigReady(config);
-      },
-
-      _viewChanged: function(view) {
-        this.set('_showChangeListView', view == 'gr-change-list-view');
-        this.set('_showDashboardView', view == 'gr-dashboard-view');
-        this.set('_showChangeView', view == 'gr-change-view');
-        this.set('_showDiffView', view == 'gr-diff-view');
-      },
-
-      _loginTapHandler: function(e) {
-        e.preventDefault();
-        page.show('/login/' + encodeURIComponent(
-            window.location.pathname + window.location.hash));
-      },
-
-      _computeLoggedIn: function(account) { // argument used for binding update only
-        return this.loggedIn;
-      },
-
-      _handleTitleChange: function(e) {
-        if (e.detail.title) {
-          document.title = e.detail.title + ' · Gerrit Code Review';
-        } else {
-          document.title = '';
-        }
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
-        switch (e.keyCode) {
-          case 191:  // '/' or '?' with shift key.
-            // TODO(andybons): Localization using e.key/keypress event.
-            if (!e.shiftKey) { break; }
-            this.$.keyboardShortcuts.open();
-        }
-      },
-
-      _handleKeyboardShortcutDialogClose: function() {
-        this.$.keyboardShortcuts.close();
-      },
-    });
-  })();
-  </script>
+  <script src="gr-app.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
new file mode 100644
index 0000000..23ee6c6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -0,0 +1,171 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-app',
+
+    properties: {
+      account: {
+        type: Object,
+        observer: '_accountChanged',
+      },
+      accountReady: {
+        type: Object,
+        readOnly: true,
+        notify: true,
+        value: function() {
+          return new Promise(function(resolve) {
+            this._resolveAccountReady = resolve;
+          }.bind(this));
+        },
+      },
+      config: {
+        type: Object,
+        observer: '_configChanged',
+      },
+      configReady: {
+        type: Object,
+        readOnly: true,
+        notify: true,
+        value: function() {
+          return new Promise(function(resolve) {
+            this._resolveConfigReady = resolve;
+          }.bind(this));
+        },
+      },
+      version: String,
+      params: Object,
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+
+      _diffPreferences: Object,
+      _showChangeListView: Boolean,
+      _showDashboardView: Boolean,
+      _showChangeView: Boolean,
+      _showDiffView: Boolean,
+      _viewState: Object,
+    },
+
+    listeners: {
+      'title-change': '_handleTitleChange',
+    },
+
+    observers: [
+      '_viewChanged(params.view)',
+    ],
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    get loggedIn() {
+      return !!(this.account && Object.keys(this.account).length > 0);
+    },
+
+    ready: function() {
+      this._viewState = {
+        changeView: {
+          changeNum: null,
+          patchNum: null,
+          selectedFileIndex: 0,
+          showReplyDialog: false,
+        },
+        changeListView: {
+          query: null,
+          offset: 0,
+          selectedChangeIndex: 0,
+        },
+        dashboardView: {
+          selectedChangeIndex: 0,
+        },
+      };
+    },
+
+    _accountChanged: function() {
+      this._resolveAccountReady();
+      this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
+      this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
+      if (this.loggedIn) {
+        this.$.diffPreferencesXHR.generateRequest();
+      } else {
+        // These defaults should match the defaults in
+        // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+        // NOTE: There are some settings that don't apply to PolyGerrit
+        // (Render mode being at least one of them).
+        this._diffPreferences = {
+          auto_hide_diff_table_header: true,
+          context: 10,
+          cursor_blink_rate: 0,
+          ignore_whitespace: 'IGNORE_NONE',
+          intraline_difference: true,
+          line_length: 100,
+          show_line_endings: true,
+          show_tabs: true,
+          show_whitespace_errors: true,
+          syntax_highlighting: true,
+          tab_size: 8,
+          theme: 'DEFAULT',
+        };
+      }
+    },
+
+    _configChanged: function(config) {
+      this._resolveConfigReady(config);
+    },
+
+    _viewChanged: function(view) {
+      this.set('_showChangeListView', view == 'gr-change-list-view');
+      this.set('_showDashboardView', view == 'gr-dashboard-view');
+      this.set('_showChangeView', view == 'gr-change-view');
+      this.set('_showDiffView', view == 'gr-diff-view');
+    },
+
+    _loginTapHandler: function(e) {
+      e.preventDefault();
+      page.show('/login/' + encodeURIComponent(
+          window.location.pathname + window.location.hash));
+    },
+
+    _computeLoggedIn: function(account) { // argument used for binding update only
+      return this.loggedIn;
+    },
+
+    _handleTitleChange: function(e) {
+      if (e.detail.title) {
+        document.title = e.detail.title + ' · Gerrit Code Review';
+      } else {
+        document.title = '';
+      }
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+
+      switch (e.keyCode) {
+        case 191:  // '/' or '?' with shift key.
+          // TODO(andybons): Localization using e.key/keypress event.
+          if (!e.shiftKey) { break; }
+          this.$.keyboardShortcuts.open();
+      }
+    },
+
+    _handleKeyboardShortcutDialogClose: function() {
+      this.$.keyboardShortcuts.close();
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/gr-avatar.html b/polygerrit-ui/app/elements/gr-avatar.html
deleted file mode 100644
index c6b14c7..0000000
--- a/polygerrit-ui/app/elements/gr-avatar.html
+++ /dev/null
@@ -1,83 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-
-<dom-module id="gr-avatar">
-  <template>
-    <style>
-      :host {
-        display: inline-block;
-        border-radius: 50%;
-        background-size: cover;
-        background-color: var(--background-color, #f1f2f3);
-      }
-    </style>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-avatar',
-
-      properties: {
-        account: {
-          type: Object,
-          observer: '_accountChanged',
-        },
-        imageSize: {
-          type: Number,
-          value: 16,
-        },
-      },
-
-      created: function() {
-        this.hidden = true;
-      },
-
-      ready: function() {
-        app.configReady.then(function(cfg) {
-          var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-          if (hasAvatars) {
-            this.hidden = false;
-            this._updateAvatarURL(this.account); // src needs to be set if avatar becomes visible
-          }
-        }.bind(this));
-      },
-
-      _accountChanged: function(account) {
-        this._updateAvatarURL(account);
-      },
-
-      _updateAvatarURL: function(account) {
-        if (!this.hidden && account) {
-          var url = this._buildAvatarURL(this.account);
-          if (url) {
-            this.style.backgroundImage = 'url("' + url + '")';
-          }
-        }
-      },
-
-      _buildAvatarURL: function(account) {
-        if (!account) { return ''; }
-        return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html
deleted file mode 100644
index 2381ce4..0000000
--- a/polygerrit-ui/app/elements/gr-change-actions.html
+++ /dev/null
@@ -1,295 +0,0 @@
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-confirm-rebase-dialog.html">
-<link rel="import" href="gr-overlay.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-change-actions">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      gr-button {
-        display: block;
-        margin-bottom: .5em;
-      }
-      gr-button:before {
-        content: attr(data-label);
-      }
-      gr-button[loading]:before {
-        content: attr(data-loading-label);
-      }
-      @media screen and (max-width: 50em) {
-        .confirmDialog {
-          width: 90vw;
-        }
-      }
-    </style>
-    <gr-ajax id="actionsXHR"
-        url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
-        last-response="{{_revisionActions}}"
-        loading="{{_loading}}"></gr-ajax>
-    <div>
-      <template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
-        <gr-button title$="[[action.title]]"
-            primary$="[[_computePrimary(action.__key)]]"
-            hidden$="[[!action.enabled]]"
-            data-action-key$="[[action.__key]]"
-            data-action-type$="[[action.__type]]"
-            data-label$="[[action.label]]"
-            on-tap="_handleActionTap"></gr-button>
-      </template>
-      <template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
-        <gr-button title$="[[action.title]]"
-            primary$="[[_computePrimary(action.__key)]]"
-            disabled$="[[!action.enabled]]"
-            data-action-key$="[[action.__key]]"
-            data-action-type$="[[action.__type]]"
-            data-label$="[[action.label]]"
-            data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
-            on-tap="_handleActionTap"></gr-button>
-      </template>
-    </div>
-    <gr-overlay id="overlay" with-backdrop>
-      <gr-confirm-rebase-dialog id="confirmRebase"
-          class="confirmDialog"
-          on-confirm="_handleRebaseConfirm"
-          on-cancel="_handleConfirmDialogCancel"
-          hidden></gr-confirm-rebase-dialog>
-    </gr-overlay>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    // TODO(davido): Add the rest of the change actions.
-    var ChangeActions = {
-      ABANDON: 'abandon',
-      DELETE: '/',
-      RESTORE: 'restore',
-    };
-
-    // TODO(andybons): Add the rest of the revision actions.
-    var RevisionActions = {
-      DELETE: '/',
-      PUBLISH: 'publish',
-      REBASE: 'rebase',
-      SUBMIT: 'submit',
-    };
-
-    Polymer({
-      is: 'gr-change-actions',
-
-      /**
-       * Fired when the change should be reloaded.
-       *
-       * @event reload-change
-       */
-
-      properties: {
-        actions: {
-          type: Object,
-        },
-        changeNum: String,
-        patchNum: String,
-        _loading: {
-          type: Boolean,
-          value: true,
-        },
-        _revisionActions: Object,
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      observers: [
-        '_actionsChanged(actions, _revisionActions)',
-      ],
-
-      reload: function() {
-        if (!this.changeNum || !this.patchNum) {
-          return Promise.resolve();
-        }
-        return this.$.actionsXHR.generateRequest().completes;
-      },
-
-      _actionsChanged: function(actions, revisionActions) {
-        this.hidden =
-            revisionActions.rebase == null &&
-            revisionActions.submit == null &&
-            revisionActions.publish == null &&
-            actions.abandon == null &&
-            actions.restore == null;
-      },
-
-      _computeRevisionActionsPath: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/actions';
-      },
-
-      _getValuesFor: function(obj) {
-        return Object.keys(obj).map(function(key) {
-          return obj[key];
-        });
-      },
-
-      _computeActionValues: function(actions, type) {
-        var result = [];
-        var values = this._getValuesFor(
-            type == 'change' ? ChangeActions : RevisionActions);
-        for (var a in actions) {
-          if (values.indexOf(a) == -1) { continue; }
-          actions[a].__key = a;
-          actions[a].__type = type;
-          result.push(actions[a]);
-        }
-        return result;
-      },
-
-      _computeLoadingLabel: function(action) {
-        return {
-          'rebase': 'Rebasing...',
-          'submit': 'Submitting...',
-        }[action];
-      },
-
-      _computePrimary: function(actionKey) {
-        return actionKey == 'submit';
-      },
-
-      _computeButtonClass: function(action) {
-        if ([RevisionActions.SUBMIT,
-            RevisionActions.PUBLISH].indexOf(action) != -1) {
-          return 'primary';
-        }
-        return '';
-      },
-
-      _handleActionTap: function(e) {
-        e.preventDefault();
-        var el = Polymer.dom(e).rootTarget;
-        var key = el.getAttribute('data-action-key');
-        var type = el.getAttribute('data-action-type');
-        if (type == 'revision') {
-          if (key == RevisionActions.REBASE) {
-            this._showRebaseDialog();
-            return;
-          }
-          this._fireRevisionAction(this._prependSlash(key),
-              this._revisionActions[key]);
-        } else {
-          this._fireChangeAction(this._prependSlash(key), this.actions[key]);
-        }
-      },
-
-      _prependSlash: function(key) {
-        return key == '/' ? key : '/' + key;
-      },
-
-      _handleConfirmDialogCancel: function() {
-        var dialogEls =
-            Polymer.dom(this.root).querySelectorAll('.confirmDialog');
-        for (var i = 0; i < dialogEls.length; i++) {
-          dialogEls[i].hidden = true;
-        }
-        this.$.overlay.close();
-      },
-
-      _handleRebaseConfirm: function() {
-        var payload = {};
-        var el = this.$.confirmRebase;
-        if (el.clearParent) {
-          // There is a subtle but important difference between setting the base
-          // to an empty string and omitting it entirely from the payload. An
-          // empty string implies that the parent should be cleared and the
-          // change should be rebased on top of the target branch. Leaving out
-          // the base implies that it should be rebased on top of its current
-          // parent.
-          payload.base = '';
-        } else if (el.base && el.base.length > 0) {
-          payload.base = el.base;
-        }
-        this.$.overlay.close();
-        el.hidden = false;
-        this._fireRevisionAction('/rebase', this._revisionActions.rebase,
-            payload);
-      },
-
-      _fireChangeAction: function(endpoint, action) {
-        this._send(action.method, {}, endpoint).then(
-          function() {
-            // We can’t reload a change that was deleted.
-            if (endpoint == ChangeActions.DELETE) {
-              page.show('/');
-            } else {
-              this.fire('reload-change', null, {bubbles: false});
-            }
-          }.bind(this)).catch(function(err) {
-            alert('Oops. Something went wrong. Check the console and bug the ' +
-                'PolyGerrit team for assistance.');
-            throw err;
-          });
-      },
-
-      _fireRevisionAction: function(endpoint, action, opt_payload) {
-        var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
-        buttonEl.setAttribute('loading', true);
-        buttonEl.disabled = true;
-        function enableButton() {
-          buttonEl.removeAttribute('loading');
-          buttonEl.disabled = false;
-        }
-
-        this._send(action.method, opt_payload, endpoint, true).then(
-          function() {
-            this.fire('reload-change', null, {bubbles: false});
-            enableButton();
-          }.bind(this)).catch(function(err) {
-            // TODO(andybons): Handle merge conflict (409 status);
-            alert('Oops. Something went wrong. Check the console and bug the ' +
-                'PolyGerrit team for assistance.');
-            enableButton();
-            throw err;
-          });
-      },
-
-      _showRebaseDialog: function() {
-        this.$.confirmRebase.hidden = false;
-        this.$.overlay.open();
-      },
-
-      _send: function(method, payload, actionEndpoint, revisionAction) {
-        var xhr = document.createElement('gr-request');
-        this._xhrPromise = xhr.send({
-          method: method,
-          url: this.changeBaseURL(this.changeNum,
-              revisionAction ? this.patchNum : null) + actionEndpoint,
-          body: payload,
-        });
-
-        return this._xhrPromise;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
deleted file mode 100644
index 1e57e09..0000000
--- a/polygerrit-ui/app/elements/gr-change-list-item.html
+++ /dev/null
@@ -1,210 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../styles/gr-change-list-styles.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-account-link.html">
-<link rel="import" href="gr-change-star.html">
-<link rel="import" href="gr-date-formatter.html">
-
-<dom-module id="gr-change-list-item">
-  <template>
-    <style>
-      :host {
-        display: flex;
-        border-bottom: 1px solid #eee;
-      }
-      :host([selected]) {
-        background-color: #ebf5fb;
-      }
-      :host([needs-review]) {
-        font-weight: bold;
-      }
-      .cell {
-        flex-shrink: 0;
-        padding: .3em .5em;
-      }
-      a {
-        color: var(--default-text-color);
-        text-decoration: none;
-      }
-      a:hover {
-        text-decoration: underline;
-      }
-      .positionIndicator {
-        visibility: hidden;
-      }
-      :host([selected]) .positionIndicator {
-        visibility: visible;
-      }
-      .u-monospace {
-        font-family: var(--monospace-font-family);
-      }
-      .u-green {
-        color: #388E3C;
-      }
-      .u-red {
-        color: #D32F2F;
-      }
-    </style>
-    <style include="gr-change-list-styles"></style>
-    <span class="cell keyboard">
-      <span class="positionIndicator">&#x25b6;</span>
-    </span>
-    <span class="cell star" hidden$="[[!showStar]]">
-      <gr-change-star change="{{change}}"></gr-change-star>
-    </span>
-    <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
-    <span class="cell status">[[_computeChangeStatusString(change)]]</span>
-    <span class="cell owner">
-      <gr-account-link account="[[change.owner]]"></gr-account-link>
-    </span>
-    <a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
-    <a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
-    <gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter>
-    <span class="cell size u-monospace">
-      <span class="u-green"><span>+</span>[[change.insertions]]</span>,
-      <span class="u-red"><span>-</span>[[change.deletions]]</span>
-    </span>
-    <template is="dom-repeat" items="[[labelNames]]" as="labelName">
-      <span title$="[[_computeLabelTitle(change, labelName)]]"
-          class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span>
-    </template>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-change-list-item',
-
-      properties: {
-        selected: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        needsReview: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        labelNames: {
-          type: Array,
-        },
-        change: Object,
-        changeURL: {
-          type: String,
-          computed: '_computeChangeURL(change._number)',
-        },
-        showStar: {
-          type: Boolean,
-          value: false,
-        },
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      _computeChangeURL: function(changeNum) {
-        if (!changeNum) { return ''; }
-        return '/c/' + changeNum + '/';
-      },
-
-      _computeChangeStatusString: function(change) {
-        if (change.status == this.ChangeStatus.MERGED) {
-          return 'Merged';
-        }
-        if (change.mergeable != null && change.mergeable == false) {
-          return 'Merge Conflict';
-        }
-        if (change.status == this.ChangeStatus.DRAFT) {
-          return 'Draft';
-        }
-        if (change.status == this.ChangeStatus.ABANDONED) {
-          return 'Abandoned';
-        }
-        return '';
-      },
-
-      _computeLabelTitle: function(change, labelName) {
-        var label = change.labels[labelName];
-        if (!label) { return labelName; }
-        var significantLabel = label.rejected || label.approved ||
-            label.disliked || label.recommended;
-        if (significantLabel && significantLabel.name) {
-          return labelName + '\nby ' + significantLabel.name;
-        }
-        return labelName;
-      },
-
-      _computeLabelClass: function(change, labelName) {
-        var label = change.labels[labelName];
-        // Mimic a Set.
-        var classes = {
-          'cell': true,
-          'label': true,
-        };
-        if (label) {
-          if (label.approved) {
-            classes['u-green'] = true;
-          }
-          if (label.value == 1) {
-            classes['u-monospace'] = true;
-            classes['u-green'] = true;
-          } else if (label.value == -1) {
-            classes['u-monospace'] = true;
-            classes['u-red'] = true;
-          }
-          if (label.rejected) {
-            classes['u-red'] = true;
-          }
-        }
-        return Object.keys(classes).sort().join(' ');
-      },
-
-      _computeLabelValue: function(change, labelName) {
-        var label = change.labels[labelName];
-        if (!label) { return ''; }
-        if (label.approved) {
-          return '✓';
-        }
-        if (label.rejected) {
-          return '✕';
-        }
-        if (label.value > 0) {
-          return '+' + label.value;
-        }
-        if (label.value < 0) {
-          return label.value;
-        }
-        return '';
-      },
-
-      _computeProjectURL: function(project) {
-        return '/projects/' + project + ',dashboards/default';
-      },
-
-      _computeProjectBranchURL: function(project, branch) {
-        return '/q/status:open+project:' + project + '+branch:' + branch;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html
deleted file mode 100644
index d6f7649..0000000
--- a/polygerrit-ui/app/elements/gr-change-list-view.html
+++ /dev/null
@@ -1,229 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-change-list.html">
-
-<dom-module id="gr-change-list-view">
-  <template>
-    <style>
-      :host {
-        background-color: var(--view-background-color);
-        display: block;
-        margin: 0 var(--default-horizontal-margin);
-      }
-      .loading,
-      .error {
-        margin-top: 1em;
-        background-color: #f1f2f3;
-      }
-      .loading {
-        color: #666;
-      }
-      .error {
-        color: #D32F2F;
-      }
-      gr-change-list {
-        margin-top: 1em;
-        width: 100%;
-      }
-      nav {
-        margin-bottom: 1em;
-        padding: .5em 0;
-        text-align: center;
-      }
-      nav a {
-        display: inline-block;
-      }
-      nav a:first-of-type {
-        margin-right: .5em;
-      }
-      @media only screen and (max-width: 50em) {
-        :host {
-          margin: 0;
-        }
-        .loading,
-        .error {
-          padding: 0 var(--default-horizontal-margin);
-        }
-      }
-    </style>
-    <gr-ajax
-        auto
-        url="/changes/"
-        params="[[_computeQueryParams(_query, _offset)]]"
-        last-response="{{_changes}}"
-        last-error="{{_lastError}}"
-        loading="{{_loading}}"></gr-ajax>
-    <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
-    <div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden>
-      [[_lastError.request.xhr.responseText]]
-    </div>
-    <div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden>
-      <gr-change-list
-          changes="{{_changes}}"
-          selected-index="{{viewState.selectedChangeIndex}}"
-          show-star="[[loggedIn]]"></gr-change-list>
-      <nav>
-        <a href$="[[_computeNavLink(_query, _offset, -1)]]"
-           hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
-        <a href$="[[_computeNavLink(_query, _offset, 1)]]"
-           hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
-      </nav>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var DEFAULT_NUM_CHANGES = 25;
-
-    Polymer({
-      is: 'gr-change-list-view',
-
-      /**
-       * Fired when the title of the page should change.
-       *
-       * @event title-change
-       */
-
-      properties: {
-        /**
-         * URL params passed from the router.
-         */
-        params: {
-          type: Object,
-          observer: '_paramsChanged',
-        },
-
-        /**
-         * True when user is logged in.
-         */
-        loggedIn: {
-          type: Boolean,
-          value: false,
-        },
-
-        /**
-         * State persisted across restamps of the element.
-         */
-        viewState: {
-          type: Object,
-          notify: true,
-          value: function() { return {}; },
-        },
-
-        /**
-         * Currently active query.
-         */
-        _query: String,
-
-        /**
-         * Offset of currently visible query results.
-         */
-        _offset: Number,
-
-        /**
-         * Change objects loaded from the server.
-         */
-        _changes: Array,
-
-        /**
-         * Contains error of last request (in case of change loading error).
-         */
-        _lastError: Object,
-
-        /**
-         * For showing a "loading..." string during ajax requests.
-         */
-        _loading: {
-          type: Boolean,
-          value: true,
-        },
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      attached: function() {
-        this.fire('title-change', {title: this._query});
-      },
-
-      _paramsChanged: function(value) {
-        if (value.view != this.tagName.toLowerCase()) { return; }
-
-        this._query = value.query;
-        this._offset = value.offset || 0;
-        if (this.viewState.query != this._query ||
-            this.viewState.offset != this._offset) {
-          this.set('viewState.selectedChangeIndex', 0);
-          this.set('viewState.query', this._query);
-          this.set('viewState.offset', this._offset);
-        }
-
-        this.fire('title-change', {title: this._query});
-      },
-
-      _computeQueryParams: function(query, offset) {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.LABELS,
-            this.ListChangesOption.DETAILED_ACCOUNTS
-        );
-        var obj = {
-          n: DEFAULT_NUM_CHANGES,  // Number of results to return.
-          O: options,
-          S: offset || 0,
-        };
-        if (query && query.length > 0) {
-          obj.q = query;
-        }
-        return obj;
-      },
-
-      _computeNavLink: function(query, offset, direction) {
-        // Offset could be a string when passed from the router.
-        offset = +(offset || 0);
-        var newOffset = Math.max(0, offset + (25 * direction));
-        var href = '/q/' + query;
-        if (newOffset > 0) {
-          href += ',' + newOffset;
-        }
-        return href;
-      },
-
-      _computeErrorHidden: function(loading, lastError) {
-        return loading || lastError == null;
-      },
-
-      _computeListHidden: function(loading, lastError) {
-        return loading || lastError != null;
-      },
-
-      _hidePrevArrow: function(offset) {
-        return offset == 0;
-      },
-
-      _hideNextArrow: function(changesLen) {
-        return changesLen < DEFAULT_NUM_CHANGES;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html
deleted file mode 100644
index 1a0341a..0000000
--- a/polygerrit-ui/app/elements/gr-change-list.html
+++ /dev/null
@@ -1,221 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="../styles/gr-change-list-styles.html">
-<link rel="import" href="gr-change-list-item.html">
-
-<dom-module id="gr-change-list">
-  <template>
-    <style>
-      :host {
-        display: flex;
-        flex-direction: column;
-      }
-    </style>
-    <style include="gr-change-list-styles"></style>
-    <div class="headerRow">
-      <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
-      <span class="topHeader star" hidden$="[[!showStar]]"></span>
-      <span class="topHeader subject">Subject</span>
-      <span class="topHeader status">Status</span>
-      <span class="topHeader owner">Owner</span>
-      <span class="topHeader project">Project</span>
-      <span class="topHeader branch">Branch</span>
-      <span class="topHeader updated">Updated</span>
-      <span class="topHeader size">Size</span>
-      <template is="dom-repeat" items="[[labelNames]]" as="labelName">
-        <span class="topHeader label" title$="[[labelName]]">
-          [[_computeLabelShortcut(labelName)]]
-        </span>
-      </template>
-    </div>
-    <template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex">
-      <template is="dom-if" if="[[_groupTitle(groupIndex)]]">
-        <div class="groupHeader">[[_groupTitle(groupIndex)]]</div>
-      </template>
-      <template is="dom-if" if="[[!changeGroup.length]]">
-        <div class="noChanges">No changes</div>
-      </template>
-      <template is="dom-repeat" items="[[changeGroup]]" as="change">
-        <gr-change-list-item
-            selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
-            needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
-            change="[[change]]"
-            show-star="[[showStar]]"
-            label-names="[[labelNames]]"></gr-change-list-item>
-      </template>
-    </template>
-  </template>
-
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-change-list',
-
-      hostAttributes: {
-        tabindex: 0,
-      },
-
-      properties: {
-        /**
-         * The logged-in user's account, or an empty object if no user is logged
-         * in.
-         */
-        account: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        /**
-         * An array of ChangeInfo objects to render.
-         * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
-         */
-        changes: {
-          type: Array,
-          observer: '_changesChanged',
-        },
-        /**
-         * ChangeInfo objects grouped into arrays. The groups and changes
-         * properties should not be used together.
-         */
-        groups: {
-          type: Array,
-          value: function() { return []; },
-        },
-        groupTitles: {
-          type: Array,
-          value: function() { return []; },
-        },
-        labelNames: {
-          type: Array,
-          computed: '_computeLabelNames(groups)',
-        },
-        selectedIndex: {
-          type: Number,
-          notify: true,
-        },
-        showStar: {
-          type: Boolean,
-          value: false,
-        },
-        showReviewedState: {
-          type: Boolean,
-          value: false,
-        },
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-        Gerrit.RESTClientBehavior,
-      ],
-
-      _computeLabelNames: function(groups) {
-        if (!groups) { return []; }
-        var labels = [];
-        var nonExistingLabel = function(item) {
-          return labels.indexOf(item) < 0;
-        };
-        for (var i = 0; i < groups.length; i++) {
-          var group = groups[i];
-          for (var j = 0; j < group.length; j++) {
-            var change = group[j];
-            if (!change.labels) { continue; }
-            var currentLabels = Object.keys(change.labels);
-            labels = labels.concat(currentLabels.filter(nonExistingLabel));
-          }
-        }
-        return labels.sort();
-      },
-
-      _computeLabelShortcut: function(labelName) {
-        return labelName.replace(/[a-z-]/g, '');
-      },
-
-      _changesChanged: function(changes) {
-        this.groups = changes ? [changes] : [];
-      },
-
-      _groupTitle: function(groupIndex) {
-        if (groupIndex > this.groupTitles.length - 1) { return null; }
-        return this.groupTitles[groupIndex];
-      },
-
-      _computeItemSelected: function(index, groupIndex, selectedIndex) {
-        var idx = 0;
-        for (var i = 0; i < groupIndex; i++) {
-          idx += this.groups[i].length;
-        }
-        idx += index;
-        return idx == selectedIndex;
-      },
-
-      _computeItemNeedsReview: function(account, change, showReviewedState) {
-        return showReviewedState && !change.reviewed &&
-            change.status != this.ChangeStatus.MERGED &&
-            account._account_id != change.owner._account_id;
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
-        if (this.groups == null) { return; }
-        var len = 0;
-        this.groups.forEach(function(group) {
-          len += group.length;
-        });
-        switch (e.keyCode) {
-          case 74:  // 'j'
-            e.preventDefault();
-            if (this.selectedIndex == len - 1) { return; }
-            this.selectedIndex += 1;
-            break;
-          case 75:  // 'k'
-            e.preventDefault();
-            if (this.selectedIndex == 0) { return; }
-            this.selectedIndex -= 1;
-            break;
-          case 79:  // 'o'
-          case 13:  // 'enter'
-            e.preventDefault();
-            page.show(this._changeURLForIndex(this.selectedIndex));
-            break;
-        }
-      },
-
-      _changeURLForIndex: function(index) {
-        var changeEls = this._getListItems();
-        if (index < changeEls.length && changeEls[index]) {
-          return changeEls[index].changeURL;
-        }
-        return '';
-      },
-
-      _getListItems: function() {
-        return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-star.html b/polygerrit-ui/app/elements/gr-change-star.html
deleted file mode 100644
index ad377ef..0000000
--- a/polygerrit-ui/app/elements/gr-change-star.html
+++ /dev/null
@@ -1,101 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-change-star">
-  <template>
-    <style>
-      :host {
-        display: inline-block;
-        overflow: hidden;
-      }
-      .starButton {
-        background-color: transparent;
-        border-color: transparent;
-        cursor: pointer;
-        font-size: 1.1em;
-        width: 1.2em;
-        height: 1.2em;
-        outline: none;
-      }
-      .starButton svg {
-        fill: #ccc;
-        width: 1em;
-        height: 1em;
-      }
-      .starButton-active svg {
-        fill: #ffac33;
-      }
-    </style>
-    <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap">
-      <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 -->
-      <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657  l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657  L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg>
-    </button>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-change-star',
-
-      properties: {
-        change: {
-          type: Object,
-          notify: true,
-        },
-
-        _xhrPromise: Object,  // Used for testing.
-      },
-
-      _computeStarClass: function(starred) {
-        var classes = ['starButton'];
-        if (starred) {
-          classes.push('starButton-active');
-        }
-        return classes.join(' ');
-      },
-
-      _handleStarTap: function() {
-        var method = this.change.starred ? 'DELETE' : 'PUT';
-        this.set('change.starred', !this.change.starred);
-        this._send(method, this._restEndpoint()).catch(function(err) {
-          this.set('change.starred', !this.change.starred);
-          alert('Change couldn’t be starred. Check the console and contact ' +
-              'the PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
-      },
-
-      _send: function(method, url) {
-        var xhr = document.createElement('gr-request');
-        this._xhrPromise = xhr.send({
-          method: method,
-          url: url,
-        });
-        return this._xhrPromise;
-      },
-
-      _restEndpoint: function() {
-        return '/accounts/self/starred.changes/' + this.change._number;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
deleted file mode 100644
index 05eb709..0000000
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ /dev/null
@@ -1,661 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-account-link.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-change-actions.html">
-<link rel="import" href="gr-change-metadata.html">
-<link rel="import" href="gr-change-star.html">
-<link rel="import" href="gr-date-formatter.html">
-<link rel="import" href="gr-download-dialog.html">
-<link rel="import" href="gr-file-list.html">
-<link rel="import" href="gr-linked-text.html">
-<link rel="import" href="gr-messages-list.html">
-<link rel="import" href="gr-overlay.html">
-<link rel="import" href="gr-related-changes-list.html">
-<link rel="import" href="gr-reply-dialog.html">
-
-<dom-module id="gr-change-view">
-  <template>
-    <style>
-      .container {
-        margin: 1em var(--default-horizontal-margin);
-      }
-      .container:not(.loading) {
-        background-color: var(--view-background-color);
-      }
-      .container.loading {
-        color: #666;
-      }
-      .headerContainer {
-        height: 4.1em;
-        margin-bottom: .5em;
-      }
-      .header {
-        align-items: center;
-        background-color: var(--view-background-color);
-        border-bottom: 1px solid #ddd;
-        display: flex;
-        padding: 1em var(--default-horizontal-margin);
-        z-index: 99;  /* Less than gr-overlay's backdrop */
-      }
-      .header.pinned {
-        border-bottom-color: transparent;
-        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
-        position: fixed;
-        top: 0;
-        transition: box-shadow 250ms linear;
-        width: calc(100% - (2 * var(--default-horizontal-margin)));
-      }
-      .header-title {
-        flex: 1;
-        font-size: 1.2em;
-        font-weight: bold;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
-      gr-change-star {
-        margin-right: .25em;
-        vertical-align: -.425em;
-      }
-      .download,
-      .patchSelectLabel {
-        margin-left: var(--default-horizontal-margin);
-      }
-      .header select {
-        margin-left: .5em;
-      }
-      .header .reply {
-        margin-left: var(--default-horizontal-margin);
-      }
-      gr-reply-dialog {
-        min-width: 30em;
-        max-width: 50em;
-      }
-      .changeStatus {
-        color: #999;
-        text-transform: capitalize;
-      }
-      section {
-        margin: 10px 0;
-        padding: 10px var(--default-horizontal-margin);
-      }
-      /* Strong specificity here is needed due to
-         https://github.com/Polymer/polymer/issues/2531 */
-      .container section.changeInfo {
-        border-bottom: 1px solid #ddd;
-        display: flex;
-        margin-top: 0;
-        padding-top: 0;
-      }
-      .changeInfo-column:not(:last-of-type) {
-        margin-right: 1em;
-        padding-right: 1em;
-      }
-      .changeMetadata {
-        border-right: 1px solid #ddd;
-        font-size: .9em;
-      }
-      gr-change-actions {
-        margin-top: 1em;
-      }
-      .commitMessage {
-        font-family: var(--monospace-font-family);
-        flex: 0 0 72ch;
-        margin-right: 2em;
-        margin-bottom: 1em;
-      }
-      .commitMessage h4 {
-        font-family: var(--font-family);
-        font-weight: bold;
-        margin-bottom: .25em;
-      }
-      .commitAndRelated {
-        align-content: flex-start;
-        display: flex;
-        flex: 1;
-        flex-wrap: wrap;
-      }
-      gr-file-list {
-        margin-bottom: 1em;
-        padding: 0 var(--default-horizontal-margin);
-      }
-      @media screen and (max-width: 50em) {
-        .container {
-          margin: .5em 0 !important;
-        }
-        .container.loading {
-          margin: 1em var(--default-horizontal-margin) !important;
-        }
-        .headerContainer {
-          height: 5.15em;
-        }
-        .header {
-          align-items: flex-start;
-          flex-direction: column;
-          padding: .5em var(--default-horizontal-margin) !important;
-        }
-        gr-change-star {
-          vertical-align: middle;
-        }
-        .header-title,
-        .header-actions,
-        .header.pinned {
-          width: 100% !important;
-        }
-        .header-title {
-          font-size: 1.1em;
-        }
-        .header-actions {
-          align-items: center;
-          display: flex;
-          justify-content: space-between;
-          margin-top: .5em;
-        }
-        gr-reply-dialog {
-          min-width: initial;
-          width: 90vw;
-        }
-        .download {
-          display: none;
-        }
-        .patchSelectLabel {
-          margin-left: 0 !important;
-          margin-right: .5em;
-        }
-        .header select {
-          margin-left: 0 !important;
-          margin-right: .5em;
-        }
-        .header .reply {
-          margin-left: 0 !important;
-          margin-right: .5em;
-        }
-        .changeInfo-column:not(:last-of-type) {
-          margin-right: 0;
-          padding-right: 0;
-        }
-        .changeInfo,
-        .commitAndRelated {
-          flex-direction: column;
-          flex-wrap: nowrap;
-        }
-        .changeMetadata {
-          font-size: 1em;
-          border-right: none;
-          margin-bottom: 1em;
-          margin-top: .25em;
-          max-width: none;
-        }
-        .commitMessage {
-          flex: initial;
-          margin-right: 0;
-        }
-      }
-    </style>
-    <gr-ajax id="detailXHR"
-        url="[[_computeDetailPath(_changeNum)]]"
-        params="[[_computeDetailQueryParams()]]"
-        last-response="{{_change}}"
-        loading="{{_loading}}"></gr-ajax>
-    <gr-ajax id="commentsXHR"
-        url="[[_computeCommentsPath(_changeNum)]]"
-        last-response="{{_comments}}"></gr-ajax>
-    <gr-ajax id="commitInfoXHR"
-        url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]"
-        last-response="{{_commitInfo}}"></gr-ajax>
-    <!-- TODO(andybons): Cache the project config. -->
-    <gr-ajax id="configXHR"
-        auto
-        url="[[_computeProjectConfigPath(_change.project)]]"
-        last-response="{{_projectConfig}}"></gr-ajax>
-    <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
-    <div class="container" hidden$="{{_loading}}">
-      <div class="headerContainer">
-        <div class="header">
-          <span class="header-title">
-            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
-            <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
-            <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
-          </span>
-          <span class="header-actions">
-            <gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button>
-            <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
-            <span>
-              <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
-              <select id="patchSetSelect" on-change="_handlePatchChange">
-                <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
-                    <span>[[patchNumber]]</span>
-                    /
-                    <span>[[_computeLatestPatchNum(_change)]]</span>
-                  </option>
-                </template>
-              </select>
-            </span>
-          </span>
-        </div>
-      </div>
-      <section class="changeInfo">
-        <div class="changeInfo-column changeMetadata">
-          <gr-change-metadata
-              change="[[_change]]"
-              mutable="[[_loggedIn]]"></gr-change-metadata>
-          <gr-change-actions id="actions"
-              actions="[[_change.actions]]"
-              change-num="[[_changeNum]]"
-              patch-num="[[_patchNum]]"
-              on-reload-change="_handleReloadChange"></gr-change-actions>
-        </div>
-        <div class="changeInfo-column commitAndRelated">
-          <div class="commitMessage">
-            <h4>Commit message</h4>
-            <gr-linked-text pre
-                content="[[_commitInfo.message]]"
-                config="[[_projectConfig.commentlinks]]"></gr-linked-text>
-          </div>
-          <div class="relatedChanges">
-            <gr-related-changes-list id="relatedChanges"
-              change="[[_change]]"
-              server-config="[[serverConfig]]"
-              patch-num="[[_patchNum]]"></gr-related-changes-list>
-          </div>
-        </div>
-      </section>
-      <gr-file-list id="fileList"
-          change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
-          comments="[[_comments]]"
-          selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
-      <gr-messages-list id="messageList"
-          change-num="[[_changeNum]]"
-          messages="[[_change.messages]]"
-          comments="[[_comments]]"
-          project-config="[[_projectConfig]]"
-          show-reply-buttons="[[_loggedIn]]"
-          on-reply="_handleMessageReply"></gr-messages-list>
-    </div>
-    <gr-overlay id="downloadOverlay" with-backdrop>
-      <gr-download-dialog
-          change="[[_change]]"
-          patch-num="[[_patchNum]]"
-          config="[[serverConfig.download]]"
-          on-close="_handleDownloadDialogClose"></gr-download-dialog>
-    </gr-overlay>
-    <gr-overlay id="replyOverlay"
-        on-iron-overlay-opened="_handleReplyOverlayOpen"
-        with-backdrop>
-      <gr-reply-dialog id="replyDialog"
-          change-num="[[_changeNum]]"
-          patch-num="[[_patchNum]]"
-          labels="[[_change.labels]]"
-          permitted-labels="[[_change.permitted_labels]]"
-          on-send="_handleReplySent"
-          on-cancel="_handleReplyCancel"
-          hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
-    </gr-overlay>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-change-view',
-
-      /**
-       * Fired when the title of the page should change.
-       *
-       * @event title-change
-       */
-
-      properties: {
-        /**
-         * URL params passed from the router.
-         */
-        params: {
-          type: Object,
-          observer: '_paramsChanged',
-        },
-        viewState: {
-          type: Object,
-          notify: true,
-          value: function() { return {}; },
-        },
-        serverConfig: Object,
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-
-        _comments: Object,
-        _change: {
-          type: Object,
-          observer: '_changeChanged',
-        },
-        _commitInfo: Object,
-        _changeNum: String,
-        _patchNum: String,
-        _allPatchSets: {
-          type: Array,
-          computed: '_computeAllPatchSets(_change)',
-        },
-        _loggedIn: {
-          type: Boolean,
-          value: false,
-        },
-        _loading: Boolean,
-        _headerContainerEl: Object,
-        _headerEl: Object,
-        _projectConfig: Object,
-        _boundScrollHandler: {
-          type: Function,
-          value: function() { return this._handleBodyScroll.bind(this); },
-        },
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-        Gerrit.RESTClientBehavior,
-      ],
-
-      ready: function() {
-        app.accountReady.then(function() {
-          this._loggedIn = app.loggedIn;
-        }.bind(this));
-        this._headerEl = this.$$('.header');
-      },
-
-      attached: function() {
-        window.addEventListener('scroll', this._boundScrollHandler);
-      },
-
-      detached: function() {
-        window.removeEventListener('scroll', this._boundScrollHandler);
-      },
-
-      _handleBodyScroll: function(e) {
-        var containerEl = this._headerContainerEl ||
-            this.$$('.headerContainer');
-
-        // Calculate where the header is relative to the window.
-        var top = containerEl.offsetTop;
-        for (var offsetParent = containerEl.offsetParent;
-             offsetParent;
-             offsetParent = offsetParent.offsetParent) {
-          top += offsetParent.offsetTop;
-        }
-        // The element may not be displayed yet, in which case do nothing.
-        if (top == 0) { return; }
-
-        this._headerEl.classList.toggle('pinned', window.scrollY >= top);
-      },
-
-      _resetHeaderEl: function() {
-        var el = this._headerEl || this.$$('.header');
-        this._headerEl = el;
-        el.classList.remove('pinned');
-      },
-
-      _handlePatchChange: function(e) {
-        var patchNum = e.target.value;
-        var currentPatchNum =
-            this._change.revisions[this._change.current_revision]._number;
-        if (patchNum == currentPatchNum) {
-          page.show(this._computeChangePath(this._changeNum));
-          return;
-        }
-        page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
-      },
-
-      _handleReplyTap: function(e) {
-        e.preventDefault();
-        this.$.replyOverlay.open();
-      },
-
-      _handleDownloadTap: function(e) {
-        e.preventDefault();
-        this.$.downloadOverlay.open();
-      },
-
-      _handleDownloadDialogClose: function(e) {
-        this.$.downloadOverlay.close();
-      },
-
-      _handleMessageReply: function(e) {
-        var msg = e.detail.message.message;
-        var quoteStr = msg.split('\n').map(
-            function(line) { return '> ' + line; }).join('\n') + '\n\n';
-        this.$.replyDialog.draft += quoteStr;
-        this.$.replyOverlay.open();
-      },
-
-      _handleReplyOverlayOpen: function(e) {
-        this.$.replyDialog.reload().then(function() {
-          this.async(function() { this.$.replyOverlay.center() }, 1);
-        }.bind(this));
-        this.$.replyDialog.focus();
-      },
-
-      _handleReplySent: function(e) {
-        this.$.replyOverlay.close();
-        this._reload();
-      },
-
-      _handleReplyCancel: function(e) {
-        this.$.replyOverlay.close();
-      },
-
-      _paramsChanged: function(value) {
-        if (value.view != this.tagName.toLowerCase()) { return; }
-
-        this._changeNum = value.changeNum;
-        this._patchNum = value.patchNum;
-        if (this.viewState.changeNum != this._changeNum ||
-            this.viewState.patchNum != this._patchNum) {
-          this.set('viewState.selectedFileIndex', 0);
-          this.set('viewState.changeNum', this._changeNum);
-          this.set('viewState.patchNum', this._patchNum);
-        }
-        if (!this._changeNum) {
-          return;
-        }
-        this._reload().then(function() {
-          this.$.messageList.topMargin = this._headerEl.offsetHeight;
-
-          // Allow the message list to render before scrolling.
-          this.async(function() {
-            var msgPrefix = '#message-';
-            var hash = window.location.hash;
-            if (hash.indexOf(msgPrefix) == 0) {
-              this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
-            }
-          }.bind(this), 1);
-
-          app.accountReady.then(function() {
-            if (!this._loggedIn) { return; }
-
-            if (this.viewState.showReplyDialog) {
-              this.$.replyOverlay.open();
-              this.set('viewState.showReplyDialog', false);
-            }
-          }.bind(this));
-        }.bind(this));
-      },
-
-      _changeChanged: function(change) {
-        if (!change) { return; }
-        this._patchNum = this._patchNum ||
-            change.revisions[change.current_revision]._number;
-
-        var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
-        this.fire('title-change', {title: title});
-      },
-
-      _computeChangePath: function(changeNum) {
-        return '/c/' + changeNum;
-      },
-
-      _computeChangePermalink: function(changeNum) {
-        return '/' + changeNum;
-      },
-
-      _computeChangeStatus: function(change, patchNum) {
-        var status = change.status;
-        if (status == this.ChangeStatus.NEW) {
-          var rev = this._getRevisionNumber(change, patchNum);
-          // TODO(davido): Figure out, why sometimes revision is not there
-          if (rev == undefined || !rev.draft) { return ''; }
-          status = this.ChangeStatus.DRAFT;
-        }
-        return '(' + status.toLowerCase() + ')';
-      },
-
-      _computeDetailPath: function(changeNum) {
-        return '/changes/' + changeNum + '/detail';
-      },
-
-      _computeCommitInfoPath: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/commit?links';
-      },
-
-      _computeCommentsPath: function(changeNum) {
-        return '/changes/' + changeNum + '/comments';
-      },
-
-      _computeProjectConfigPath: function(project) {
-        return '/projects/' + encodeURIComponent(project) + '/config';
-      },
-
-      _computeDetailQueryParams: function() {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.ALL_REVISIONS,
-            this.ListChangesOption.CHANGE_ACTIONS,
-            this.ListChangesOption.DOWNLOAD_COMMANDS
-        );
-        return {O: options};
-      },
-
-      _computeLatestPatchNum: function(change) {
-        return change.revisions[change.current_revision]._number;
-      },
-
-      _computeAllPatchSets: function(change) {
-        var patchNums = [];
-        for (var rev in change.revisions) {
-          patchNums.push(change.revisions[rev]._number);
-        }
-        return patchNums.sort(function(a, b) {
-          return a - b;
-        });
-      },
-
-      _getRevisionNumber: function(change, patchNum) {
-        for (var rev in change.revisions) {
-          if (change.revisions[rev]._number == patchNum) {
-            return change.revisions[rev];
-          }
-        }
-      },
-
-      _computePatchIndexIsSelected: function(index, patchNum) {
-        return this._allPatchSets[index] == patchNum;
-      },
-
-      _computeLabelNames: function(labels) {
-        return Object.keys(labels).sort();
-      },
-
-      _computeLabelValues: function(labelName, labels) {
-        var result = [];
-        var t = labels[labelName];
-        if (!t) { return result; }
-        var approvals = t.all || [];
-        approvals.forEach(function(label) {
-          if (label.value && label.value != labels[labelName].default_value) {
-            var labelClassName;
-            var labelValPrefix = '';
-            if (label.value > 0) {
-              labelValPrefix = '+';
-              labelClassName = 'approved';
-            } else if (label.value < 0) {
-              labelClassName = 'notApproved';
-            }
-            result.push({
-              value: labelValPrefix + label.value,
-              className: labelClassName,
-              account: label,
-            });
-          }
-        });
-        return result;
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
-        switch (e.keyCode) {
-          case 65:  // 'a'
-            e.preventDefault();
-            this.$.replyOverlay.open();
-            break;
-          case 85:  // 'u'
-            e.preventDefault();
-            page.show('/');
-            break;
-        }
-      },
-
-      _handleReloadChange: function() {
-        page.show(this._computeChangePath(this._changeNum));
-      },
-
-      _reload: function() {
-        var detailCompletes = this.$.detailXHR.generateRequest().completes;
-        this.$.commentsXHR.generateRequest();
-        var reloadPatchNumDependentResources = function() {
-          return Promise.all([
-            this.$.commitInfoXHR.generateRequest().completes,
-            this.$.actions.reload(),
-            this.$.fileList.reload(),
-          ]);
-        }.bind(this);
-        var reloadDetailDependentResources = function() {
-          return this.$.relatedChanges.reload();
-        }.bind(this);
-
-        this._resetHeaderEl();
-
-        if (this._patchNum) {
-          return reloadPatchNumDependentResources().then(function() {
-            return detailCompletes;
-          }).then(reloadDetailDependentResources);
-        } else {
-          // The patch number is reliant on the change detail request.
-          return detailCompletes.then(reloadPatchNumDependentResources).then(
-              reloadDetailDependentResources);
-        }
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-comment-list.html b/polygerrit-ui/app/elements/gr-comment-list.html
deleted file mode 100644
index 6fb5dae..0000000
--- a/polygerrit-ui/app/elements/gr-comment-list.html
+++ /dev/null
@@ -1,118 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-
-<dom-module id="gr-comment-list">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      .file {
-        border-top: 1px solid #ddd;
-        font-weight: bold;
-        margin: 10px 0 3px;
-        padding: 10px 0 5px;
-      }
-      .container {
-        display: flex;
-        margin: 5px 0;
-      }
-      .lineNum {
-        margin-right: .35em;
-        min-width: 7em;
-      }
-      .message {
-        flex: 1;
-        white-space: pre-wrap;
-        word-wrap: break-word;
-      }
-    </style>
-    <template is="dom-repeat" items="{{_files}}" as="file">
-      <div class="file">
-        <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
-      </div>
-      <template is="dom-repeat"
-                items="[[_computeCommentsForFile(file)]]" as="comment">
-        <div class="container">
-          <a class="lineNum"
-             href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
-             <span hidden$="[[!comment.line]]">
-               <span>[[_computePatchDisplayName(comment)]]</span>
-               Line <span>[[comment.line]]</span>:
-             </span>
-             <span hidden$="[[comment.line]]">
-               File comment:
-             </span>
-          </a>
-          <div class="message">[[comment.message]]</div>
-        </div>
-      </template>
-    </template>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-comment-list',
-
-      properties: {
-        changeNum: Number,
-        comments: {
-          type: Object,
-          observer: '_commentsChanged',
-        },
-        patchNum: Number,
-
-        _files: Array,
-      },
-
-      _commentsChanged: function(value) {
-        this._files = Object.keys(value || {}).sort();
-      },
-
-      _computeFileDiffURL: function(file, changeNum, patchNum) {
-        return '/c/' + changeNum + '/' + patchNum + '/' + file;
-      },
-
-      _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
-        var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
-        if (comment.line) {
-          // TODO(andybons): This is not correct if the comment is on the base.
-          diffURL += '#' + comment.line;
-        }
-        return diffURL;
-      },
-
-      _computeCommentsForFile: function(file) {
-        return this.comments[file];
-      },
-
-      _computePatchDisplayName: function(comment) {
-        if (comment.side == 'PARENT') {
-          return 'Base, ';
-        }
-        if (comment.patch_set != this.patchNum) {
-          return 'PS' + comment.patch_set + ', ';
-        }
-        return '';
-      }
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-confirm-dialog.html b/polygerrit-ui/app/elements/gr-confirm-dialog.html
deleted file mode 100644
index a82e31c..0000000
--- a/polygerrit-ui/app/elements/gr-confirm-dialog.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-button.html">
-
-<dom-module id="gr-confirm-dialog">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      header {
-        border-bottom: 1px solid #ddd;
-        font-weight: bold;
-      }
-      header,
-      main,
-      footer {
-        padding: .5em .65em;
-      }
-      footer {
-        display: flex;
-        justify-content: space-between;
-      }
-    </style>
-    <header><content select=".header"></content></header>
-    <main><content select=".main"></content></main>
-    <footer>
-      <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button>
-      <gr-button on-tap="_handleCancelTap">Cancel</gr-button>
-    </footer>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-confirm-dialog',
-
-      /**
-       * Fired when the confirm button is pressed.
-       *
-       * @event confirm
-       */
-
-      /**
-       * Fired when the cancel button is pressed.
-       *
-       * @event cancel
-       */
-
-      properties: {
-        confirmLabel: {
-          type: String,
-          value: 'Confirm',
-        }
-      },
-
-      hostAttributes: {
-        role: 'dialog',
-      },
-
-      _handleConfirmTap: function(e) {
-        e.preventDefault();
-        this.fire('confirm', null, {bubbles: false});
-      },
-
-      _handleCancelTap: function(e) {
-        e.preventDefault();
-        this.fire('cancel', null, {bubbles: false});
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-dashboard-view.html b/polygerrit-ui/app/elements/gr-dashboard-view.html
deleted file mode 100644
index cdd4b1a..0000000
--- a/polygerrit-ui/app/elements/gr-dashboard-view.html
+++ /dev/null
@@ -1,129 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-
-<dom-module id="gr-dashboard-view">
-  <template>
-    <style>
-      :host {
-        background-color: var(--view-background-color);
-        display: block;
-        margin: 0 var(--default-horizontal-margin);
-      }
-      .loading {
-        margin-top: 1em;
-        color: #666;
-        background-color: #f1f2f3;
-      }
-      gr-change-list {
-        margin-top: 1em;
-        width: 100%;
-      }
-      @media only screen and (max-width: 50em) {
-        :host {
-          margin: 0;
-        }
-        .loading {
-          padding: 0 var(--default-horizontal-margin);
-        }
-      }
-    </style>
-    <gr-ajax
-        auto
-        url="/changes/"
-        params="[[_computeQueryParams()]]"
-        last-response="{{_results}}"
-        loading="{{_loading}}"></gr-ajax>
-    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <gr-change-list
-          show-star
-          show-reviewed-state
-          account="[[account]]"
-          selected-index="{{viewState.selectedChangeIndex}}"
-          groups="{{_results}}"
-          group-titles="[[_groupTitles]]"></gr-change-list>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-dashboard-view',
-
-      /**
-       * Fired when the title of the page should change.
-       *
-       * @event title-change
-       */
-
-      properties: {
-        account: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        viewState: Object,
-
-        _results: Array,
-        _groupTitles: {
-          type: Array,
-          value: [
-            'Outgoing reviews',
-            'Incoming reviews',
-            'Recently closed',
-          ],
-        },
-
-        /**
-         * For showing a "loading..." string during ajax requests.
-         */
-        _loading: {
-          type: Boolean,
-          value: true,
-        },
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      attached: function() {
-        this.fire('title-change', {title: 'My Reviews'});
-      },
-
-      _computeQueryParams: function() {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.LABELS,
-            this.ListChangesOption.DETAILED_ACCOUNTS,
-            this.ListChangesOption.REVIEWED
-        );
-        return {
-          O: options,
-          q: [
-            'is:open owner:self',
-            'is:open reviewer:self -owner:self',
-            'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
-          ],
-        };
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-date-formatter.html b/polygerrit-ui/app/elements/gr-date-formatter.html
deleted file mode 100644
index a13dbb8..0000000
--- a/polygerrit-ui/app/elements/gr-date-formatter.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-
-<dom-module id="gr-date-formatter">
-  <template>
-    <style>
-      :host {
-        display: inline;
-      }
-    </style>
-    <span>[[_computeDateStr(dateStr)]]</span>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var Duration = {
-      HOUR: 1000 * 60 * 60,
-      DAY: 1000 * 60 * 60 * 24,
-    };
-
-    var ShortMonthNames = [
-      'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
-      'Nov', 'Dec'
-    ];
-
-    Polymer({
-      is: 'gr-date-formatter',
-
-      properties: {
-        dateStr: {
-          type: String,
-          value: null,
-          notify: true
-        }
-      },
-
-      _computeDateStr: function(dateStr) {
-        return this._dateStr(this._parseDateStr(dateStr), new Date());
-      },
-
-      _parseDateStr: function(dateStr) {
-        if (!dateStr) { return null; }
-        return util.parseDate(dateStr);
-      },
-
-      _dateStr: function(t, now) {
-        if (!t) { return ''; }
-        var diff = now.getTime() - t.getTime();
-        if (diff < Duration.DAY && t.getDay() == now.getDay()) {
-          // Within 24 hours and on the same day:
-          // '2:14 AM'
-          var pm = t.getHours() >= 12;
-          var hours = t.getHours();
-          if (hours == 0) {
-            hours = 12;
-          } else if (hours > 12) {
-            hours = t.getHours() - 12;
-          }
-          var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
-              t.getMinutes();
-          return hours + ':' + minutes + (pm ? ' PM' : ' AM');
-        } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
-                   diff < 180 * Duration.DAY) {
-          // From one to six months:
-          // 'Aug 29'
-          return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
-        } else if (diff >= 180 * Duration.DAY) {
-          // More than six months:
-          // 'Aug 29, 1997'
-          return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
-              t.getFullYear();
-        }
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/gr-diff-comment-thread.html
deleted file mode 100644
index 178b417..0000000
--- a/polygerrit-ui/app/elements/gr-diff-comment-thread.html
+++ /dev/null
@@ -1,257 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-diff-comment.html">
-
-<dom-module id="gr-diff-comment-thread">
-  <template>
-    <style>
-      :host {
-        display: block;
-        white-space: normal;
-      }
-      gr-diff-comment {
-        border-left: 1px solid #ddd;
-      }
-      gr-diff-comment:first-of-type {
-        border-top: 1px solid #ddd;
-      }
-      gr-diff-comment:last-of-type {
-        border-bottom: 1px solid #ddd;
-      }
-    </style>
-    <div id="container">
-      <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
-        <gr-diff-comment
-            comment="{{comment}}"
-            change-num="[[changeNum]]"
-            patch-num="[[patchNum]]"
-            draft="[[comment.__draft]]"
-            show-actions="[[showActions]]"
-            project-config="[[projectConfig]]"
-            on-height-change="_handleCommentHeightChange"
-            on-reply="_handleCommentReply"
-            on-discard="_handleCommentDiscard"
-            on-done="_handleCommentDone"></gr-diff-comment>
-      </template>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-diff-comment-thread',
-
-      /**
-       * Fired when the height of the thread changes.
-       *
-       * @event height-change
-       */
-
-      /**
-       * Fired when the thread should be discarded.
-       *
-       * @event discard
-       */
-
-      properties: {
-        changeNum: String,
-        comments: {
-          type: Array,
-          value: function() { return []; },
-        },
-        patchNum: String,
-        path: String,
-        showActions: Boolean,
-        projectConfig: Object,
-
-        _boundWindowResizeHandler: {
-          type: Function,
-          value: function() { return this._handleWindowResize.bind(this); }
-        },
-        _lastHeight: Number,
-        _orderedComments: Array,
-      },
-
-      get naturalHeight() {
-        return this.$.container.offsetHeight;
-      },
-
-      observers: [
-        '_commentsChanged(comments.splices)',
-      ],
-
-      attached: function() {
-        window.addEventListener('resize', this._boundWindowResizeHandler);
-      },
-
-      detached: function() {
-        window.removeEventListener('resize', this._boundWindowResizeHandler);
-      },
-
-      _handleWindowResize: function(e) {
-        this._heightChanged();
-      },
-
-      _commentsChanged: function(changeRecord) {
-        this._orderedComments = this._sortedComments(this.comments);
-      },
-
-      _sortedComments: function(comments) {
-        comments.sort(function(c1, c2) {
-          var c1Date = c1.__date || util.parseDate(c1.updated);
-          var c2Date = c2.__date || util.parseDate(c2.updated);
-          return c1Date - c2Date;
-        });
-
-        var commentIDToReplies = {};
-        var topLevelComments = [];
-        for (var i = 0; i < comments.length; i++) {
-          var c = comments[i];
-          if (c.in_reply_to) {
-            if (commentIDToReplies[c.in_reply_to] == null) {
-              commentIDToReplies[c.in_reply_to] = [];
-            }
-            commentIDToReplies[c.in_reply_to].push(c);
-          } else {
-            topLevelComments.push(c);
-          }
-        }
-        var results = [];
-        for (var i = 0; i < topLevelComments.length; i++) {
-          this._visitComment(topLevelComments[i], commentIDToReplies, results);
-        }
-        return results;
-      },
-
-      _visitComment: function(parent, commentIDToReplies, results) {
-        results.push(parent);
-
-        var replies = commentIDToReplies[parent.id];
-        if (!replies) { return; }
-        for (var i = 0; i < replies.length; i++) {
-          this._visitComment(replies[i], commentIDToReplies, results);
-        }
-      },
-
-      _handleCommentHeightChange: function(e) {
-        e.stopPropagation();
-        this._heightChanged();
-      },
-
-      _handleCommentReply: function(e) {
-        var comment = e.detail.comment;
-        var quoteStr;
-        if (e.detail.quote) {
-          var msg = comment.message;
-          var quoteStr = msg.split('\n').map(
-              function(line) { return ' > ' + line; }).join('\n') + '\n\n';
-        }
-        var reply =
-            this._newReply(comment.id, comment.line, this.path, quoteStr);
-        this.push('comments', reply);
-
-        // Allow the reply to render in the dom-repeat.
-        this.async(function() {
-          var commentEl = this._commentElWithDraftID(reply.__draftID);
-          commentEl.editing = true;
-          this.async(this._heightChanged.bind(this), 1);
-        }.bind(this), 1);
-      },
-
-      _handleCommentDone: function(e) {
-        var comment = e.detail.comment;
-        var reply = this._newReply(comment.id, comment.line, this.path, 'Done');
-        this.push('comments', reply);
-
-        // Allow the reply to render in the dom-repeat.
-        this.async(function() {
-          var commentEl = this._commentElWithDraftID(reply.__draftID);
-          commentEl.save();
-          this.async(this._heightChanged.bind(this), 1);
-        }.bind(this), 1);
-      },
-
-      _commentElWithDraftID: function(draftID) {
-        var commentEls =
-            Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
-        for (var i = 0; i < commentEls.length; i++) {
-          if (commentEls[i].comment.__draftID == draftID) {
-            return commentEls[i];
-          }
-        }
-        return null;
-      },
-
-      _newReply: function(inReplyTo, line, path, opt_message) {
-        var c = {
-          __draft: true,
-          __draftID: Math.random().toString(36),
-          __date: new Date(),
-          line: line,
-          path: path,
-          in_reply_to: inReplyTo,
-        };
-        if (opt_message != null) {
-          c.message = opt_message;
-        }
-        return c;
-      },
-
-      _handleCommentDiscard: function(e) {
-        // TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady
-        // DOM, it respects the bubbles property.
-        // https://github.com/Polymer/polymer/issues/3226
-        e.stopPropagation();
-        var diffCommentEl = Polymer.dom(e).rootTarget;
-        var idx = this._indexOf(diffCommentEl.comment, this.comments);
-        if (idx == -1) {
-          throw Error('Cannot find comment ' +
-              JSON.stringify(diffCommentEl.comment));
-        }
-        this.splice('comments', idx, 1);
-        if (this.comments.length == 0) {
-          this.fire('discard', null, {bubbles: false});
-          return;
-        }
-        this.async(this._heightChanged.bind(this), 1);
-      },
-
-      _heightChanged: function() {
-        var height = this.$.container.offsetHeight;
-        if (height == this._lastHeight) { return; }
-
-        this.fire('height-change', {height: height}, {bubbles: false});
-        this._lastHeight = height;
-      },
-
-      _indexOf: function(comment, arr) {
-        for (var i = 0; i < arr.length; i++) {
-          var c = arr[i];
-          if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
-              (c.id != null && c.id == comment.id)) {
-            return i;
-          }
-        }
-        return -1;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html
deleted file mode 100644
index c544a8d..0000000
--- a/polygerrit-ui/app/elements/gr-diff-comment.html
+++ /dev/null
@@ -1,389 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-date-formatter.html">
-<link rel="import" href="gr-linked-text.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-diff-comment">
-  <template>
-    <style>
-      :host {
-        background-color: #ffd;
-        display: block;
-        --iron-autogrow-textarea: {
-          padding: 2px;
-        };
-      }
-      :host([disabled]) {
-        pointer-events: none;
-      }
-      :host([disabled]) .container {
-        opacity: .5;
-      }
-      .header,
-      .message,
-      .actions {
-        padding: .5em .7em;
-      }
-      .header {
-        display: flex;
-        padding-bottom: 0;
-        font-family: 'Open Sans', sans-serif;
-      }
-      .headerLeft {
-        flex: 1;
-      }
-      .authorName,
-      .draftLabel {
-        font-weight: bold;
-      }
-      .draftLabel {
-        color: #999;
-        display: none;
-      }
-      .date {
-        justify-content: flex-end;
-        margin-left: 5px;
-      }
-      a.date:link,
-      a.date:visited {
-        color: #666;
-      }
-      .actions {
-        display: flex;
-        padding-top: 0;
-      }
-      .action {
-        margin-right: .5em;
-      }
-      .danger {
-        display: flex;
-        flex: 1;
-        justify-content: flex-end;
-      }
-      .editMessage {
-        display: none;
-        margin: .5em .7em;
-        width: calc(100% - 1.4em - 2px);
-      }
-      .danger .action {
-        margin-right: 0;
-      }
-      .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) {
-        display: none;
-      }
-      .draft .reply,
-      .draft .quote,
-      .draft .done {
-        display: none;
-      }
-      .draft .draftLabel {
-        display: inline;
-      }
-      .draft:not(.editing) .save,
-      .draft:not(.editing) .cancel {
-        display: none;
-      }
-      .editing .message,
-      .editing .reply,
-      .editing .quote,
-      .editing .done,
-      .editing .edit {
-        display: none;
-      }
-      .editing .editMessage {
-        background-color: #fff;
-        display: block;
-      }
-    </style>
-    <div class="container" id="container">
-      <div class="header" id="header">
-        <div class="headerLeft">
-          <span class="authorName">[[comment.author.name]]</span>
-          <span class="draftLabel">DRAFT</span>
-        </div>
-        <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
-          <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
-        </a>
-      </div>
-      <iron-autogrow-textarea
-          id="editTextarea"
-          class="editMessage"
-          disabled="{{disabled}}"
-          rows="4"
-          bind-value="{{_editDraft}}"
-          on-keyup="_handleTextareaKeyup"
-          on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
-      <gr-linked-text class="message"
-          pre
-          content="[[comment.message]]"
-          config="[[projectConfig.commentlinks]]"></gr-linked-text>
-      <div class="actions" hidden$="[[!showActions]]">
-        <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
-        <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
-        <gr-button class="action done" on-tap="_handleDone">Done</gr-button>
-        <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
-        <gr-button class="action save" on-tap="_handleSave"
-            disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
-        <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
-        <div class="danger">
-          <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
-        </div>
-      </div>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-diff-comment',
-
-      /**
-       * Fired when the height of the comment changes.
-       *
-       * @event height-change
-       */
-
-      /**
-       * Fired when the Reply action is triggered.
-       *
-       * @event reply
-       */
-
-      /**
-       * Fired when the Done action is triggered.
-       *
-       * @event done
-       */
-
-      /**
-       * Fired when this comment is discarded.
-       *
-       * @event discard
-       */
-
-      properties: {
-        changeNum: String,
-        comment: {
-          type: Object,
-          notify: true,
-        },
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        draft: {
-          type: Boolean,
-          value: false,
-          observer: '_draftChanged',
-        },
-        editing: {
-          type: Boolean,
-          value: false,
-          observer: '_editingChanged',
-        },
-        patchNum: String,
-        showActions: Boolean,
-        projectConfig: Object,
-
-        _xhrPromise: Object,  // Used for testing.
-        _editDraft: String,
-      },
-
-      ready: function() {
-        this._editDraft = (this.comment && this.comment.message) || '';
-        this.editing = this._editDraft.length == 0;
-      },
-
-      attached: function() {
-        this._heightChanged();
-      },
-
-      save: function() {
-        this.comment.message = this._editDraft;
-        this.disabled = true;
-        var endpoint = this._restEndpoint(this.comment.id);
-        this._send('PUT', endpoint).then(function(req) {
-          this.disabled = false;
-          var comment = req.response;
-          comment.__draft = true;
-          // Maintain the ephemeral draft ID for identification by other
-          // elements.
-          if (this.comment.__draftID) {
-            comment.__draftID = this.comment.__draftID;
-          }
-          this.comment = comment;
-          this.editing = false;
-        }.bind(this)).catch(function(err) {
-          alert('Your draft couldn’t be saved. Check the console and contact ' +
-              'the PolyGerrit team for assistance.');
-          this.disabled = false;
-        }.bind(this));
-      },
-
-      _heightChanged: function() {
-        this.async(function() {
-          this.fire('height-change', {height: this.offsetHeight},
-              {bubbles: false});
-        }.bind(this));
-      },
-
-      _draftChanged: function(draft) {
-        this.$.container.classList.toggle('draft', draft);
-      },
-
-      _editingChanged: function(editing) {
-        this.$.container.classList.toggle('editing', editing);
-        if (editing) {
-          var textarea = this.$.editTextarea.textarea;
-          // Put the cursor at the end always.
-          textarea.selectionStart = textarea.value.length;
-          textarea.selectionEnd = textarea.selectionStart;
-          this.async(function() {
-            textarea.focus();
-          }.bind(this));
-        }
-        if (this.comment && this.comment.id) {
-          this.$$('.cancel').hidden = !editing;
-        }
-        this._heightChanged();
-      },
-
-      _computeLinkToComment: function(comment) {
-        return '#' + comment.line;
-      },
-
-      _computeSaveDisabled: function(draft) {
-        return draft == null || draft.trim() == '';
-      },
-
-      _handleTextareaKeyup: function(e) {
-        // TODO(andybons): This isn't always true, but I can't currently think
-        // of a better metric.
-        this._heightChanged();
-      },
-
-      _handleTextareaKeydown: function(e) {
-        if (e.keyCode == 27) {  // 'esc'
-          this._handleCancel(e);
-        }
-      },
-
-      _handleLinkTap: function(e) {
-        e.preventDefault();
-        var hash = this._computeLinkToComment(this.comment);
-        // Don't add the hash to the window history if it's already there.
-        // Otherwise you mess up expected back button behavior.
-        if (window.location.hash == hash) { return; }
-        // Change the URL but don’t trigger a nav event. Otherwise it will
-        // reload the page.
-        page.show(window.location.pathname + hash, null, false);
-      },
-
-      _handleReply: function(e) {
-        this._preventDefaultAndBlur(e);
-        this.fire('reply', {comment: this.comment}, {bubbles: false});
-      },
-
-      _handleQuote: function(e) {
-        this._preventDefaultAndBlur(e);
-        this.fire('reply', {comment: this.comment, quote: true},
-            {bubbles: false});
-      },
-
-      _handleDone: function(e) {
-        this._preventDefaultAndBlur(e);
-        this.fire('done', {comment: this.comment}, {bubbles: false});
-      },
-
-      _handleEdit: function(e) {
-        this._preventDefaultAndBlur(e);
-        this._editDraft = this.comment.message;
-        this.editing = true;
-      },
-
-      _handleSave: function(e) {
-        this._preventDefaultAndBlur(e);
-        this.save();
-      },
-
-      _handleCancel: function(e) {
-        this._preventDefaultAndBlur(e);
-        if (this.comment.message == null || this.comment.message.length == 0) {
-          this.fire('discard', null, {bubbles: false});
-          return;
-        }
-        this._editDraft = this.comment.message;
-        this.editing = false;
-      },
-
-      _handleDiscard: function(e) {
-        this._preventDefaultAndBlur(e);
-        if (!this.comment.__draft) {
-          throw Error('Cannot discard a non-draft comment.');
-        }
-        this.disabled = true;
-        var commentID = this.comment.id;
-        if (!commentID) {
-          this.fire('discard', null, {bubbles: false});
-          return;
-        }
-        this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
-          this.fire('discard', null, {bubbles: false});
-        }.bind(this)).catch(function(err) {
-          alert('Your draft couldn’t be deleted. Check the console and ' +
-              'contact the PolyGerrit team for assistance.');
-          this.disabled = false;
-        }.bind(this));
-      },
-
-      _preventDefaultAndBlur: function(e) {
-        e.preventDefault();
-        Polymer.dom(e).rootTarget.blur();
-      },
-
-      _send: function(method, url) {
-        var xhr = document.createElement('gr-request');
-        var opts = {
-          method: method,
-          url: url,
-        };
-        if (method == 'PUT' || method == 'POST') {
-          opts.body = this.comment;
-        }
-        this._xhrPromise = xhr.send(opts);
-        return this._xhrPromise;
-      },
-
-      _restEndpoint: function(id) {
-        var path = '/changes/' + this.changeNum + '/revisions/' +
-            this.patchNum + '/drafts';
-        if (id) {
-          path += '/' + id;
-        }
-        return path;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-side.html b/polygerrit-ui/app/elements/gr-diff-side.html
deleted file mode 100644
index 09ed5f8..0000000
--- a/polygerrit-ui/app/elements/gr-diff-side.html
+++ /dev/null
@@ -1,698 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-diff-comment-thread.html">
-
-<dom-module id="gr-diff-side">
-  <template>
-    <style>
-      :host,
-      .container {
-        display: flex;
-        flex: 0 0 auto;
-      }
-      .lineNum:before,
-      .code:before {
-        /* To ensure the height is non-zero in these elements, a
-           zero-width space is set as its content. The character
-           itself doesn't matter. Just that there is something
-           there. */
-        content: '\200B';
-      }
-      .lineNum {
-        background-color: #eee;
-        color: #666;
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .canComment .lineNum {
-        cursor: pointer;
-        text-decoration: underline;
-      }
-      .canComment .lineNum:hover {
-        background-color: #ccc;
-      }
-      .lightHighlight {
-        background-color: var(--light-highlight-color);
-      }
-      hl,
-      .darkHighlight {
-        background-color: var(--dark-highlight-color);
-      }
-      .br:after {
-        /* Line feed */
-        content: '\A';
-      }
-      .tab {
-        display: inline-block;
-      }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
-      }
-      .numbers,
-      .content {
-        white-space: pre;
-      }
-      .numbers .filler {
-        background-color: #eee;
-      }
-      .contextControl {
-        background-color: #fef;
-      }
-      .contextControl a:link,
-      .contextControl a:visited {
-        display: block;
-        text-decoration: none;
-      }
-      .numbers .contextControl {
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .content .contextControl {
-        text-align: center;
-      }
-    </style>
-    <div class$="[[_computeContainerClass(canComment)]]">
-      <div class="numbers" id="numbers"></div>
-      <div class="content" id="content"></div>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var CharCode = {
-      LESS_THAN: '<'.charCodeAt(0),
-      GREATER_THAN: '>'.charCodeAt(0),
-      AMPERSAND: '&'.charCodeAt(0),
-      SEMICOLON: ';'.charCodeAt(0),
-    };
-
-    var TAB_REGEX = /\t/g;
-
-    Polymer({
-      is: 'gr-diff-side',
-
-      /**
-       * Fired when an expand context control is clicked.
-       *
-       * @event expand-context
-       */
-
-      /**
-       * Fired when a thread's height is changed.
-       *
-       * @event thread-height-change
-       */
-
-      /**
-       * Fired when a draft should be added.
-       *
-       * @event add-draft
-       */
-
-      /**
-       * Fired when a thread is removed.
-       *
-       * @event remove-thread
-       */
-
-      properties: {
-        canComment: {
-          type: Boolean,
-          value: false,
-        },
-        content: {
-          type: Array,
-          notify: true,
-          observer: '_contentChanged',
-        },
-        prefs: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        changeNum: String,
-        patchNum: String,
-        path: String,
-        projectConfig: {
-          type: Object,
-          observer: '_projectConfigChanged',
-        },
-
-        _lineFeedHTML: {
-          type: String,
-          value: '<span class="style-scope gr-diff-side br"></span>',
-          readOnly: true,
-        },
-        _highlightStartTag: {
-          type: String,
-          value: '<hl class="style-scope gr-diff-side">',
-          readOnly: true,
-        },
-        _highlightEndTag: {
-          type: String,
-          value: '</hl>',
-          readOnly: true,
-        },
-        _diffChunkLineNums: {
-          type: Array,
-          value: function() { return []; },
-        },
-        _commentThreadLineNums: {
-          type: Array,
-          value: function() { return []; },
-        },
-        _focusedLineNum: {
-          type: Number,
-          value: 1,
-        },
-      },
-
-      listeners: {
-        'tap': '_tapHandler',
-      },
-
-      observers: [
-        '_prefsChanged(prefs.*)',
-      ],
-
-      rowInserted: function(index) {
-        this.renderLineIndexRange(index, index);
-        this._updateDOMIndices();
-        this._updateJumpIndices();
-      },
-
-      rowRemoved: function(index) {
-        var removedEls = Polymer.dom(this.root).querySelectorAll(
-            '[data-index="' + index + '"]');
-        for (var i = 0; i < removedEls.length; i++) {
-          removedEls[i].parentNode.removeChild(removedEls[i]);
-        }
-        this._updateDOMIndices();
-        this._updateJumpIndices();
-      },
-
-      rowUpdated: function(index) {
-        var removedEls = Polymer.dom(this.root).querySelectorAll(
-            '[data-index="' + index + '"]');
-        for (var i = 0; i < removedEls.length; i++) {
-          removedEls[i].parentNode.removeChild(removedEls[i]);
-        }
-        this.renderLineIndexRange(index, index);
-      },
-
-      scrollToLine: function(lineNum) {
-        if (isNaN(lineNum) || lineNum < 1) { return; }
-
-        var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
-        if (!el) { return; }
-
-        // Calculate where the line is relative to the window.
-        var top = el.offsetTop;
-        for (var offsetParent = el.offsetParent;
-             offsetParent;
-             offsetParent = offsetParent.offsetParent) {
-          top += offsetParent.offsetTop;
-        }
-
-        // Scroll the element to the middle of the window. Dividing by a third
-        // instead of half the inner height feels a bit better otherwise the
-        // element appears to be below the center of the window even when it
-        // isn't.
-        window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
-      },
-
-      scrollToNextDiffChunk: function() {
-        this._scrollToNextChunkOrThread(this._diffChunkLineNums);
-      },
-
-      scrollToPreviousDiffChunk: function() {
-        this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
-      },
-
-      scrollToNextCommentThread: function() {
-        this._scrollToNextChunkOrThread(this._commentThreadLineNums);
-      },
-
-      scrollToPreviousCommentThread: function() {
-        this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
-      },
-
-      renderLineIndexRange: function(startIndex, endIndex) {
-        this._render(this.content, startIndex, endIndex);
-      },
-
-      hideElementsWithIndex: function(index) {
-        var els = Polymer.dom(this.root).querySelectorAll(
-            '[data-index="' + index + '"]');
-        for (var i = 0; i < els.length; i++) {
-          els[i].setAttribute('hidden', true);
-        }
-      },
-
-      getRowHeight: function(index) {
-        var row = this.content[index];
-        // Filler elements should not be taken into account when determining
-        // height calculations.
-        if (row.type == 'FILLER') {
-          return 0;
-        }
-        if (row.height != null) {
-          return row.height;
-        }
-
-        var selector = '[data-index="' + index + '"]';
-        var els = Polymer.dom(this.root).querySelectorAll(selector);
-        if (els.length != 2) {
-          throw Error('Rows should only consist of two elements');
-        }
-        return Math.max(els[0].offsetHeight, els[1].offsetHeight);
-      },
-
-      getRowNaturalHeight: function(index) {
-        var contentEl = this.$$('.content [data-index="' + index + '"]');
-        return contentEl.naturalHeight || contentEl.offsetHeight;
-      },
-
-      setRowNaturalHeight: function(index) {
-        var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-        var contentEl = this.$$('.content [data-index="' + index + '"]');
-        contentEl.style.height = null;
-        var height = contentEl.offsetHeight;
-        lineEl.style.height = height + 'px';
-        this.content[index].height = height;
-        return height;
-      },
-
-      setRowHeight: function(index, height) {
-        var selector = '[data-index="' + index + '"]';
-        var els = Polymer.dom(this.root).querySelectorAll(selector);
-        for (var i = 0; i < els.length; i++) {
-          els[i].style.height = height + 'px';
-        }
-        this.content[index].height = height;
-      },
-
-      _scrollToNextChunkOrThread: function(lineNums) {
-        for (var i = 0; i < lineNums.length; i++) {
-          if (lineNums[i] > this._focusedLineNum) {
-            this._focusedLineNum = lineNums[i];
-            this.scrollToLine(this._focusedLineNum);
-            return;
-          }
-        }
-      },
-
-      _scrollToPreviousChunkOrThread: function(lineNums) {
-        for (var i = lineNums.length - 1; i >= 0; i--) {
-          if (this._focusedLineNum > lineNums[i]) {
-            this._focusedLineNum = lineNums[i];
-            this.scrollToLine(this._focusedLineNum);
-            return;
-          }
-        }
-      },
-
-      _updateJumpIndices: function() {
-        this._commentThreadLineNums = [];
-        this._diffChunkLineNums = [];
-        var inHighlight = false;
-        for (var i = 0; i < this.content.length; i++) {
-          switch (this.content[i].type) {
-            case 'COMMENT_THREAD':
-              this._commentThreadLineNums.push(
-                  this.content[i].comments[0].line);
-              break;
-            case 'CODE':
-              // Only grab the first line of the highlighted chunk.
-              if (!inHighlight && this.content[i].highlight) {
-                this._diffChunkLineNums.push(this.content[i].lineNum);
-                inHighlight = true;
-              } else if (!this.content[i].highlight) {
-                inHighlight = false;
-              }
-              break;
-          }
-        }
-      },
-
-      _updateDOMIndices: function() {
-        // There is no way to select elements with a data-index greater than a
-        // given value. For now, just update all DOM elements.
-        var lineEls = Polymer.dom(this.root).querySelectorAll(
-            '.numbers [data-index]');
-        var contentEls = Polymer.dom(this.root).querySelectorAll(
-            '.content [data-index]');
-        if (lineEls.length != contentEls.length) {
-          throw Error(
-              'There must be the same number of line and content elements');
-        }
-        var index = 0;
-        for (var i = 0; i < this.content.length; i++) {
-          if (this.content[i].hidden) { continue; }
-
-          lineEls[index].setAttribute('data-index', i);
-          contentEls[index].setAttribute('data-index', i);
-          index++;
-        }
-      },
-
-      _prefsChanged: function(changeRecord) {
-        var prefs = changeRecord.base;
-        this.$.content.style.width = prefs.line_length + 'ch';
-      },
-
-      _projectConfigChanged: function(projectConfig) {
-        var threadEls =
-            Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
-        for (var i = 0; i < threadEls.length; i++) {
-          threadEls[i].projectConfig = projectConfig;
-        }
-      },
-
-      _contentChanged: function(diff) {
-        this._clearChildren(this.$.numbers);
-        this._clearChildren(this.$.content);
-        this._render(diff, 0, diff.length - 1);
-        this._updateJumpIndices();
-      },
-
-      _computeContainerClass: function(canComment) {
-        return 'container' + (canComment ? ' canComment' : '');
-      },
-
-      _tapHandler: function(e) {
-        var lineEl = Polymer.dom(e).rootTarget;
-        if (!this.canComment || !lineEl.classList.contains('lineNum')) {
-          return;
-        }
-
-        e.preventDefault();
-        var index = parseInt(lineEl.getAttribute('data-index'), 10);
-        var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
-        this.fire('add-draft', {
-          index: index,
-          line: line
-        }, {bubbles: false});
-      },
-
-      _clearChildren: function(el) {
-        while (el.firstChild) {
-          el.removeChild(el.firstChild);
-        }
-      },
-
-      _handleContextControlClick: function(context, e) {
-        e.preventDefault();
-        this.fire('expand-context', {context: context}, {bubbles: false});
-      },
-
-      _render: function(diff, startIndex, endIndex) {
-        var beforeLineEl;
-        var beforeContentEl;
-        if (endIndex != diff.length - 1) {
-          beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
-          beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
-          if (!beforeLineEl && !beforeContentEl) {
-            // `endIndex` may be present within the model, but not in the DOM.
-            // Insert it before its successive element.
-            beforeLineEl = this.$$(
-                '.numbers [data-index="' + (endIndex + 1) + '"]');
-            beforeContentEl = this.$$(
-                '.content [data-index="' + (endIndex + 1) + '"]');
-          }
-        }
-
-        for (var i = startIndex; i <= endIndex; i++) {
-          if (diff[i].hidden) { continue; }
-
-          switch (diff[i].type) {
-            case 'CODE':
-              this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
-              break;
-            case 'FILLER':
-              this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
-              break;
-            case 'CONTEXT_CONTROL':
-              this._renderContextControl(diff[i], i, beforeLineEl,
-                  beforeContentEl);
-              break;
-            case 'COMMENT_THREAD':
-              this._renderCommentThread(diff[i], i, beforeLineEl,
-                  beforeContentEl);
-              break;
-          }
-        }
-      },
-
-      _handleCommentThreadHeightChange: function(e) {
-        var threadEl = Polymer.dom(e).rootTarget;
-        var index = parseInt(threadEl.getAttribute('data-index'), 10);
-        this.content[index].height = e.detail.height;
-        var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-        lineEl.style.height = e.detail.height + 'px';
-        this.fire('thread-height-change', {
-          index: index,
-          height: e.detail.height,
-        }, {bubbles: false});
-      },
-
-      _handleCommentThreadDiscard: function(e) {
-        var threadEl = Polymer.dom(e).rootTarget;
-        var index = parseInt(threadEl.getAttribute('data-index'), 10);
-        this.fire('remove-thread', {index: index}, {bubbles: false});
-      },
-
-      _renderCommentThread: function(thread, index, beforeLineEl,
-          beforeContentEl) {
-        var lineEl = this._createElement('div', 'commentThread');
-        lineEl.classList.add('filler');
-        lineEl.setAttribute('data-index', index);
-        var threadEl = document.createElement('gr-diff-comment-thread');
-        threadEl.addEventListener('height-change',
-            this._handleCommentThreadHeightChange.bind(this));
-        threadEl.addEventListener('discard',
-            this._handleCommentThreadDiscard.bind(this));
-        threadEl.setAttribute('data-index', index);
-        threadEl.changeNum = this.changeNum;
-        threadEl.patchNum = thread.patchNum || this.patchNum;
-        threadEl.path = this.path;
-        threadEl.comments = thread.comments;
-        threadEl.showActions = this.canComment;
-        threadEl.projectConfig = this.projectConfig;
-
-        this.$.numbers.insertBefore(lineEl, beforeLineEl);
-        this.$.content.insertBefore(threadEl, beforeContentEl);
-      },
-
-      _renderContextControl: function(control, index, beforeLineEl,
-          beforeContentEl) {
-        var lineEl = this._createElement('div', 'contextControl');
-        lineEl.setAttribute('data-index', index);
-        lineEl.textContent = '@@';
-        var contentEl = this._createElement('div', 'contextControl');
-        contentEl.setAttribute('data-index', index);
-        var a = this._createElement('a');
-        a.href = '#';
-        a.textContent = 'Show ' + control.numLines + ' common ' +
-            (control.numLines == 1 ? 'line' : 'lines') + '...';
-        a.addEventListener('click',
-            this._handleContextControlClick.bind(this, control));
-        contentEl.appendChild(a);
-
-        this.$.numbers.insertBefore(lineEl, beforeLineEl);
-        this.$.content.insertBefore(contentEl, beforeContentEl);
-      },
-
-      _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
-        var lineFillerEl = this._createElement('div', 'filler');
-        lineFillerEl.setAttribute('data-index', index);
-        var fillerEl = this._createElement('div', 'filler');
-        fillerEl.setAttribute('data-index', index);
-        var numLines = filler.numLines || 1;
-
-        lineFillerEl.textContent = '\n'.repeat(numLines);
-        for (var i = 0; i < numLines; i++) {
-          var newlineEl = this._createElement('span', 'br');
-          fillerEl.appendChild(newlineEl);
-        }
-
-        this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
-        this.$.content.insertBefore(fillerEl, beforeContentEl);
-      },
-
-      _renderCode: function(code, index, beforeLineEl, beforeContentEl) {
-        var lineNumEl = this._createElement('div', 'lineNum');
-        lineNumEl.setAttribute('data-line-num', code.lineNum);
-        lineNumEl.setAttribute('data-index', index);
-        var numLines = code.numLines || 1;
-        lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
-
-        var contentEl = this._createElement('div', 'code');
-        contentEl.setAttribute('data-line-num', code.lineNum);
-        contentEl.setAttribute('data-index', index);
-
-        if (code.highlight) {
-          contentEl.classList.add(code.intraline.length > 0 ?
-              'lightHighlight' : 'darkHighlight');
-        }
-
-        var html = util.escapeHTML(code.content);
-        if (code.highlight && code.intraline.length > 0) {
-          html = this._addIntralineHighlights(code.content, html,
-              code.intraline);
-        }
-        if (numLines > 1) {
-          html = this._addNewLines(code.content, html, numLines);
-        }
-        html = this._addTabWrappers(code.content, html);
-
-        // If the html is equivalent to the text then it didn't get highlighted
-        // or escaped. Use textContent which is faster than innerHTML.
-        if (code.content == html) {
-          contentEl.textContent = code.content;
-        } else {
-          contentEl.innerHTML = html;
-        }
-
-        this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
-        this.$.content.insertBefore(contentEl, beforeContentEl);
-      },
-
-      // Advance `index` by the appropriate number of characters that would
-      // represent one source code character and return that index. For
-      // example, for source code '<span>' the escaped html string is
-      // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
-      // return 4, since &lt; maps to one source code character ('<').
-      _advanceChar: function(html, index) {
-        // Any tags don't count as characters
-        while (index < html.length &&
-               html.charCodeAt(index) == CharCode.LESS_THAN) {
-          while (index < html.length &&
-                 html.charCodeAt(index) != CharCode.GREATER_THAN) {
-            index++;
-          }
-          index++;  // skip the ">" itself
-        }
-        // An HTML entity (e.g., &lt;) counts as one character.
-        if (index < html.length &&
-            html.charCodeAt(index) == CharCode.AMPERSAND) {
-          while (index < html.length &&
-                 html.charCodeAt(index) != CharCode.SEMICOLON) {
-            index++;
-          }
-        }
-        return index + 1;
-      },
-
-      _addIntralineHighlights: function(content, html, highlights) {
-        var startTag = this._highlightStartTag;
-        var endTag = this._highlightEndTag;
-
-        for (var i = 0; i < highlights.length; i++) {
-          var hl = highlights[i];
-
-          var htmlStartIndex = 0;
-          for (var j = 0; j < hl.startIndex; j++) {
-            htmlStartIndex = this._advanceChar(html, htmlStartIndex);
-          }
-
-          var htmlEndIndex = 0;
-          if (hl.endIndex != null) {
-            for (var j = 0; j < hl.endIndex; j++) {
-              htmlEndIndex = this._advanceChar(html, htmlEndIndex);
-            }
-          } else {
-            // If endIndex isn't present, continue to the end of the line.
-            htmlEndIndex = html.length;
-          }
-          // The start and end indices could be the same if a highlight is meant
-          // to start at the end of a line and continue onto the next one.
-          // Ignore it.
-          if (htmlStartIndex != htmlEndIndex) {
-            html = html.slice(0, htmlStartIndex) + startTag +
-                  html.slice(htmlStartIndex, htmlEndIndex) + endTag +
-                  html.slice(htmlEndIndex);
-          }
-        }
-        return html;
-      },
-
-      _addNewLines: function(content, html, numLines) {
-        var htmlIndex = 0;
-        var indices = [];
-        var numChars = 0;
-        for (var i = 0; i < content.length; i++) {
-          if (numChars > 0 && numChars % this.prefs.line_length == 0) {
-            indices.push(htmlIndex);
-          }
-          htmlIndex = this._advanceChar(html, htmlIndex);
-          if (content[i] == '\t') {
-            numChars += this.prefs.tab_size;
-          } else {
-            numChars++;
-          }
-        }
-        var result = html;
-        var linesLeft = numLines;
-        // Since the result string is being altered in place, start from the end
-        // of the string so that the insertion indices are not affected as the
-        // result string changes.
-        for (var i = indices.length - 1; i >= 0; i--) {
-          result = result.slice(0, indices[i]) + this._lineFeedHTML +
-              result.slice(indices[i]);
-          linesLeft--;
-        }
-        // numLines is the total number of lines this code block should take up.
-        // Fill in the remaining ones.
-        for (var i = 0; i < linesLeft; i++) {
-          result += this._lineFeedHTML;
-        }
-        return result;
-      },
-
-      _addTabWrappers: function(content, html) {
-        // TODO(andybons): CSS tab-size is not supported in IE.
-        // Force this to be a number to prevent arbitrary injection.
-        var tabSize = +this.prefs.tab_size;
-        var htmlStr = '<span class="style-scope gr-diff-side tab ' +
-            (this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
-            'style="tab-size:' + tabSize + ';' +
-            '-moz-tab-size:' + tabSize + ';">\t</span>';
-        return html.replace(TAB_REGEX, htmlStr);
-      },
-
-      _createElement: function(tagName, className) {
-        var el = document.createElement(tagName);
-        // When Shady DOM is being used, these classes are added to account for
-        // Polymer's polyfill behavior. In order to guarantee sufficient
-        // specificity within the CSS rules, these are added to every element.
-        // Since the Polymer DOM utility functions (which would do this
-        // automatically) are not being used for performance reasons, this is
-        // done manually.
-        el.classList.add('style-scope', 'gr-diff-side');
-        if (!!className) {
-          el.classList.add(className);
-        }
-        return el;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
deleted file mode 100644
index 761dd71..0000000
--- a/polygerrit-ui/app/elements/gr-diff-view.html
+++ /dev/null
@@ -1,477 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-diff.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-diff-view">
-  <template>
-    <style>
-      :host {
-        background-color: var(--view-background-color);
-        display: block;
-      }
-      h3 {
-        margin-top: 1em;
-        padding: .75em var(--default-horizontal-margin);
-      }
-      .reviewed {
-        display: inline-block;
-        margin: 0 .25em;
-        vertical-align: .15em;
-      }
-      .jumpToFileContainer {
-        display: inline-block;
-      }
-      .mobileJumpToFileContainer {
-        display: none;
-      }
-      .downArrow {
-        display: inline-block;
-        font-size: .6em;
-        vertical-align: middle;
-      }
-      .dropdown-trigger {
-        color: #00e;
-        cursor: pointer;
-        padding: 0;
-      }
-      .dropdown-content {
-        background-color: #fff;
-        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
-      }
-      .dropdown-content a {
-        cursor: pointer;
-        display: block;
-        font-weight: normal;
-        padding: .3em .5em;
-      }
-      .dropdown-content a:before {
-        color: #ccc;
-        content: attr(data-key-nav);
-        display: inline-block;
-        margin-right: .5em;
-        width: .3em;
-      }
-      .dropdown-content a:hover {
-        background-color: #00e;
-        color: #fff;
-      }
-      .dropdown-content a[selected] {
-        color: #000;
-        font-weight: bold;
-        pointer-events: none;
-        text-decoration: none;
-      }
-      .dropdown-content a[selected]:hover {
-        background-color: #fff;
-        color: #000;
-      }
-      gr-button {
-        font: inherit;
-        padding: .3em 0;
-        text-decoration: none;
-      }
-      @media screen and (max-width: 50em) {
-        .dash {
-          display: none;
-        }
-        .reviewed {
-          vertical-align: -.1em;
-        }
-        .jumpToFileContainer {
-          display: none;
-        }
-        .mobileJumpToFileContainer {
-          display: block;
-          width: 100%;
-        }
-      }
-    </style>
-    <gr-ajax id="changeDetailXHR"
-        auto
-        url="[[_computeChangeDetailPath(_changeNum)]]"
-        params="[[_computeChangeDetailQueryParams()]]"
-        last-response="{{_change}}"></gr-ajax>
-    <gr-ajax id="filesXHR"
-        auto
-        url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
-        on-response="_handleFilesResponse"></gr-ajax>
-    <gr-ajax id="configXHR"
-        auto
-        url="[[_computeProjectConfigPath(_change.project)]]"
-        last-response="{{_projectConfig}}"></gr-ajax>
-    <h3>
-      <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
-        [[_changeNum]]</a><span>:</span>
-      <span>[[_change.subject]]</span>
-      <span class="dash">—</span>
-      <input id="reviewed"
-          class="reviewed"
-          type="checkbox"
-          on-change="_handleReviewedChange"
-          hidden$="[[!_loggedIn]]" hidden>
-      <div class="jumpToFileContainer">
-        <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
-          <span>[[_computeFileDisplayName(_path)]]</span>
-          <span class="downArrow">&#9660;</span>
-        </gr-button>
-        <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
-          <div class="dropdown-content">
-            <template is="dom-repeat" items="[[_fileList]]" as="path">
-              <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
-                 selected$="[[_computeFileSelected(path, _path)]]"
-                 data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
-                 on-tap="_handleFileTap">
-                 [[_computeFileDisplayName(path)]]
-              </a>
-            </template>
-          </div>
-        </iron-dropdown>
-      </div>
-      <div class="mobileJumpToFileContainer">
-        <select on-change="_handleMobileSelectChange">
-          <template is="dom-repeat" items="[[_fileList]]" as="path">
-            <option
-                value$="[[path]]"
-                selected$="[[_computeFileSelected(path, _path)]]">
-              [[_computeFileDisplayName(path)]]
-            </option>
-          </template>
-        </select>
-      </div>
-    </h3>
-    <gr-diff id="diff"
-        change-num="[[_changeNum]]"
-        prefs="{{prefs}}"
-        patch-range="[[_patchRange]]"
-        path="[[_path]]"
-        project-config="[[_projectConfig]]"
-        available-patches="[[_computeAvailablePatches(_change.revisions)]]"
-        on-render="_handleDiffRender">
-    </gr-diff>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-
-    Polymer({
-      is: 'gr-diff-view',
-
-      /**
-       * Fired when the title of the page should change.
-       *
-       * @event title-change
-       */
-
-      properties: {
-        prefs: {
-          type: Object,
-          notify: true,
-        },
-        /**
-         * URL params passed from the router.
-         */
-        params: {
-          type: Object,
-          observer: '_paramsChanged',
-        },
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-        changeViewState: {
-          type: Object,
-          notify: true,
-          value: function() { return {}; },
-        },
-
-        _patchRange: Object,
-        _change: Object,
-        _changeNum: String,
-        _diff: Object,
-        _fileList: {
-          type: Array,
-          value: function() { return []; },
-        },
-        _path: {
-          type: String,
-          observer: '_pathChanged',
-        },
-        _loggedIn: {
-          type: Boolean,
-          value: false,
-        },
-        _xhrPromise: Object,  // Used for testing.
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-        Gerrit.RESTClientBehavior,
-      ],
-
-      ready: function() {
-        app.accountReady.then(function() {
-          this._loggedIn = app.loggedIn;
-          if (this._loggedIn) {
-            this._setReviewed(true);
-          }
-        }.bind(this));
-      },
-
-      attached: function() {
-        if (this._path) {
-          this.fire('title-change',
-              {title: this._computeFileDisplayName(this._path)});
-        }
-        window.addEventListener('resize', this._boundWindowResizeHandler);
-      },
-
-      detached: function() {
-        window.removeEventListener('resize', this._boundWindowResizeHandler);
-      },
-
-      _handleReviewedChange: function(e) {
-        this._setReviewed(Polymer.dom(e).rootTarget.checked);
-      },
-
-      _setReviewed: function(reviewed) {
-        this.$.reviewed.checked = reviewed;
-        var method = reviewed ? 'PUT' : 'DELETE';
-        var url = this.changeBaseURL(this._changeNum,
-            this._patchRange.patchNum) + '/files/' +
-            encodeURIComponent(this._path) + '/reviewed';
-        this._send(method, url).catch(function(err) {
-          alert('Couldn’t change file review status. Check the console ' +
-              'and contact the PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
-        switch (e.keyCode) {
-          case 219:  // '['
-            e.preventDefault();
-            this._navToFile(this._fileList, -1);
-            break;
-          case 221:  // ']'
-            e.preventDefault();
-            this._navToFile(this._fileList, 1);
-            break;
-          case 78:  // 'n'
-            if (e.shiftKey) {
-              this.$.diff.scrollToNextCommentThread();
-            } else {
-              this.$.diff.scrollToNextDiffChunk();
-            }
-            break;
-          case 80:  // 'p'
-            if (e.shiftKey) {
-              this.$.diff.scrollToPreviousCommentThread();
-            } else {
-              this.$.diff.scrollToPreviousDiffChunk();
-            }
-            break;
-          case 65:  // 'a'
-            if (!this._loggedIn) { return; }
-
-            this.set('changeViewState.showReplyDialog', true);
-            /* falls through */ // required by JSHint
-          case 85:  // 'u'
-            if (this._changeNum && this._patchRange.patchNum) {
-              e.preventDefault();
-              page.show(this._computeChangePath(
-                  this._changeNum,
-                  this._patchRange.patchNum,
-                  this._change && this._change.revisions));
-            }
-            break;
-          case 188:  // ','
-            this.$.diff.showDiffPreferences();
-            break;
-        }
-      },
-
-      _handleDiffRender: function() {
-        if (window.location.hash.length > 0) {
-          this.$.diff.scrollToLine(
-              parseInt(window.location.hash.substring(1), 10));
-        }
-      },
-
-      _navToFile: function(fileList, direction) {
-        if (fileList.length == 0) { return; }
-
-        var idx = fileList.indexOf(this._path) + direction;
-        if (idx < 0 || idx > fileList.length - 1) {
-          page.show(this._computeChangePath(
-              this._changeNum,
-              this._patchRange.patchNum,
-              this._change && this._change.revisions));
-          return;
-        }
-        page.show(this._computeDiffURL(this._changeNum,
-                                       this._patchRange,
-                                       fileList[idx]));
-      },
-
-      _paramsChanged: function(value) {
-        if (value.view != this.tagName.toLowerCase()) { return; }
-
-        this._changeNum = value.changeNum;
-        this._patchRange = {
-          patchNum: value.patchNum,
-          basePatchNum: value.basePatchNum || 'PARENT',
-        };
-        this._path = value.path;
-
-        this.fire('title-change',
-            {title: this._computeFileDisplayName(this._path)});
-
-        // When navigating away from the page, there is a possibility that the
-        // patch number is no longer a part of the URL (say when navigating to
-        // the top-level change info view) and therefore undefined in `params`.
-        if (!this._patchRange.patchNum) {
-          return;
-        }
-
-        this.$.diff.reload();
-      },
-
-      _pathChanged: function(path) {
-        if (this._fileList.length == 0) { return; }
-
-        this.set('changeViewState.selectedFileIndex',
-            this._fileList.indexOf(path));
-
-        if (this._loggedIn) {
-          this._setReviewed(true);
-        }
-      },
-
-      _computeDiffURL: function(changeNum, patchRange, path) {
-        var patchStr = patchRange.patchNum;
-        if (patchRange.basePatchNum != null &&
-            patchRange.basePatchNum != 'PARENT') {
-          patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
-        }
-        return '/c/' + changeNum + '/' + patchStr + '/' + path;
-      },
-
-      _computeAvailablePatches: function(revisions) {
-        var patchNums = [];
-        for (var rev in revisions) {
-          patchNums.push(revisions[rev]._number);
-        }
-        return patchNums.sort(function(a, b) { return a - b; });
-      },
-
-      _computeChangePath: function(changeNum, patchNum, revisions) {
-        var base = '/c/' + changeNum + '/';
-
-        // The change may not have loaded yet, making revisions unavailable.
-        if (!revisions) {
-          return base + patchNum;
-        }
-
-        var latestPatchNum = -1;
-        for (var rev in revisions) {
-          latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
-        }
-        if (parseInt(patchNum, 10) != latestPatchNum) {
-          return base + patchNum;
-        }
-
-        return base;
-      },
-
-      _computeFileDisplayName: function(path) {
-        return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
-      },
-
-      _computeChangeDetailPath: function(changeNum) {
-        return '/changes/' + changeNum + '/detail';
-      },
-
-      _computeChangeDetailQueryParams: function() {
-        return {O: this.listChangesOptionsToHex(
-            this.ListChangesOption.ALL_REVISIONS
-        )};
-      },
-
-      _computeFilesPath: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/files';
-      },
-
-      _computeProjectConfigPath: function(project) {
-        return '/projects/' + encodeURIComponent(project) + '/config';
-      },
-
-      _computeFileSelected: function(path, currentPath) {
-        return path == currentPath;
-      },
-
-      _computeKeyNav: function(path, selectedPath, fileList) {
-        var selectedIndex = fileList.indexOf(selectedPath);
-        if (fileList.indexOf(path) == selectedIndex - 1) {
-          return '[';
-        }
-        if (fileList.indexOf(path) == selectedIndex + 1) {
-          return ']';
-        }
-        return '';
-      },
-
-      _handleFileTap: function(e) {
-        this.$.dropdown.close();
-      },
-
-      _handleMobileSelectChange: function(e) {
-        var path = Polymer.dom(e).rootTarget.value;
-        page.show(
-            this._computeDiffURL(this._changeNum, this._patchRange, path));
-      },
-
-      _handleFilesResponse: function(e, req) {
-        this._fileList = Object.keys(e.detail.response).sort();
-      },
-
-      _showDropdownTapHandler: function(e) {
-        this.$.dropdown.open();
-      },
-
-      _send: function(method, url) {
-        var xhr = document.createElement('gr-request');
-        this._xhrPromise = xhr.send({
-          method: method,
-          url: url,
-        });
-        return this._xhrPromise;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html
deleted file mode 100644
index 39b1c9c..0000000
--- a/polygerrit-ui/app/elements/gr-diff.html
+++ /dev/null
@@ -1,857 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-diff-preferences.html">
-<link rel="import" href="gr-diff-side.html">
-<link rel="import" href="gr-overlay.html">
-<link rel="import" href="gr-patch-range-select.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-diff">
-  <template>
-    <style>
-      .loading {
-        padding: 0 var(--default-horizontal-margin) 1em;
-        color: #666;
-      }
-      .header {
-        display: flex;
-        justify-content: space-between;
-        margin: 0 var(--default-horizontal-margin) .75em;
-      }
-      .prefsButton {
-        text-align: right;
-      }
-      .diffContainer {
-        border-bottom: 1px solid #eee;
-        border-top: 1px solid #eee;
-        display: flex;
-        font: 12px var(--monospace-font-family);
-        overflow-x: auto;
-      }
-      gr-diff-side:first-of-type {
-        --light-highlight-color: #fee;
-        --dark-highlight-color: #ffd4d4;
-      }
-      gr-diff-side:last-of-type {
-        --light-highlight-color: #efe;
-        --dark-highlight-color: #d4ffd4;
-        border-right: 1px solid #ddd;
-      }
-    </style>
-    <gr-ajax id="diffXHR"
-        url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
-        params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
-        last-response="{{_diffResponse}}"
-        loading="{{_loading}}"></gr-ajax>
-    <gr-ajax id="baseCommentsXHR"
-        url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
-    <gr-ajax id="commentsXHR"
-        url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
-    <gr-ajax id="baseDraftsXHR"
-        url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
-    <gr-ajax id="draftsXHR"
-        url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
-    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <div class="header">
-        <gr-patch-range-select
-            path="[[path]]"
-            change-num="[[changeNum]]"
-            patch-range="[[patchRange]]"
-            available-patches="[[availablePatches]]"></gr-patch-range-select>
-        <gr-button link
-           class="prefsButton"
-           on-tap="_handlePrefsTap"
-           hidden$="[[!prefs]]"
-           hidden>Diff View Preferences</gr-button>
-      </div>
-      <gr-overlay id="prefsOverlay" with-backdrop>
-        <gr-diff-preferences
-            prefs="{{prefs}}"
-            on-save="_handlePrefsSave"
-            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
-      </gr-overlay>
-
-      <div class="diffContainer">
-        <gr-diff-side id="leftDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.basePatchNum]]"
-            path="[[path]]"
-            content="{{_diff.leftSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
-        <gr-diff-side id="rightDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.patchNum]]"
-            path="[[path]]"
-            content="{{_diff.rightSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
-      </div>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-diff',
-
-      /**
-       * Fired when the diff is rendered.
-       *
-       * @event render
-       */
-
-      properties: {
-        availablePatches: Array,
-        changeNum: String,
-        /*
-         * A single object to encompass basePatchNum and patchNum is used
-         * so that both can be set at once without incremental observers
-         * firing after each property changes.
-         */
-        patchRange: Object,
-        path: String,
-        prefs: {
-          type: Object,
-          notify: true,
-        },
-        projectConfig: Object,
-
-        _prefsReady: {
-          type: Object,
-          readOnly: true,
-          value: function() {
-            return new Promise(function(resolve) {
-              this._resolvePrefsReady = resolve;
-            }.bind(this));
-          },
-        },
-        _baseComments: Array,
-        _comments: Array,
-        _drafts: Array,
-        _baseDrafts: Array,
-        /**
-         * Base (left side) comments and drafts grouped by line number.
-         * Only used for initial rendering.
-         */
-        _groupedBaseComments: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        /**
-         * Comments and drafts (right side) grouped by line number.
-         * Only used for initial rendering.
-         */
-        _groupedComments: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        _diffResponse: Object,
-        _diff: {
-          type: Object,
-          value: function() { return {}; },
-        },
-        _loggedIn: {
-          type: Boolean,
-          value: false,
-        },
-        _initialRenderComplete: {
-          type: Boolean,
-          value: false,
-        },
-        _loading: {
-          type: Boolean,
-          value: true,
-        },
-        _savedPrefs: Object,
-
-        _diffRequestsPromise: Object,  // Used for testing.
-        _diffPreferencesPromise: Object,  // Used for testing.
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      observers: [
-        '_prefsChanged(prefs.*)',
-      ],
-
-      ready: function() {
-        app.accountReady.then(function() {
-          this._loggedIn = app.loggedIn;
-        }.bind(this));
-      },
-
-      scrollToLine: function(lineNum) {
-        // TODO(andybons): Should this always be the right side?
-        this.$.rightDiff.scrollToLine(lineNum);
-      },
-
-      scrollToNextDiffChunk: function() {
-        this.$.rightDiff.scrollToNextDiffChunk();
-      },
-
-      scrollToPreviousDiffChunk: function() {
-        this.$.rightDiff.scrollToPreviousDiffChunk();
-      },
-
-      scrollToNextCommentThread: function() {
-        this.$.rightDiff.scrollToNextCommentThread();
-      },
-
-      scrollToPreviousCommentThread: function() {
-        this.$.rightDiff.scrollToPreviousCommentThread();
-      },
-
-      reload: function(changeNum, patchRange, path) {
-        // If a diff takes a considerable amount of time to render, the previous
-        // diff can end up showing up while the DOM is constructed. Clear the
-        // content on a reload to prevent this.
-        this._diff = {
-          leftSide: [],
-          rightSide: [],
-        };
-
-        var promises = [
-          this._prefsReady,
-          this.$.diffXHR.generateRequest().completes
-        ];
-
-        var basePatchNum = this.patchRange.basePatchNum;
-
-        return app.accountReady.then(function() {
-          promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
-          this._diffRequestsPromise = Promise.all(promises).then(function() {
-            this._render();
-          }.bind(this)).catch(function(err) {
-            alert('Oops. Something went wrong. Check the console and bug the ' +
-                'PolyGerrit team for assistance.');
-            throw err;
-          });
-        }.bind(this));
-      },
-
-      showDiffPreferences: function() {
-        this.$.prefsOverlay.open();
-      },
-
-      _prefsChanged: function(changeRecord) {
-        if (this._initialRenderComplete) {
-          this._render();
-        }
-        this._resolvePrefsReady(changeRecord.base);
-      },
-
-      _render: function() {
-        this._groupCommentsAndDrafts();
-        this._processContent();
-
-        // Allow for the initial rendering to complete before firing the event.
-        this.async(function() {
-          this.fire('render', null, {bubbles: false});
-        }.bind(this), 1);
-
-        this._initialRenderComplete = true;
-      },
-
-      _getCommentsAndDrafts: function(basePatchNum, loggedIn) {
-        function onlyParent(c) { return c.side == 'PARENT'; }
-        function withoutParent(c) { return c.side != 'PARENT'; }
-
-        var promises = [];
-        var commentsPromise = this.$.commentsXHR.generateRequest().completes;
-        promises.push(commentsPromise.then(function(req) {
-          var comments = req.response[this.path] || [];
-          if (basePatchNum == 'PARENT') {
-            this._baseComments = comments.filter(onlyParent);
-          }
-          this._comments = comments.filter(withoutParent);
-        }.bind(this)));
-
-        if (basePatchNum != 'PARENT') {
-          commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
-          promises.push(commentsPromise.then(function(req) {
-            this._baseComments =
-              (req.response[this.path] || []).filter(withoutParent);
-          }.bind(this)));
-        }
-
-        if (!loggedIn) {
-          this._baseDrafts = [];
-          this._drafts = [];
-          return Promise.all(promises);
-        }
-
-        var draftsPromise = this.$.draftsXHR.generateRequest().completes;
-        promises.push(draftsPromise.then(function(req) {
-          var drafts = req.response[this.path] || [];
-          if (basePatchNum == 'PARENT') {
-            this._baseDrafts = drafts.filter(onlyParent);
-          }
-          this._drafts = drafts.filter(withoutParent);
-        }.bind(this)));
-
-        if (basePatchNum != 'PARENT') {
-          draftsPromise = this.$.baseDraftsXHR.generateRequest().completes;
-          promises.push(draftsPromise.then(function(req) {
-            this._baseDrafts =
-                (req.response[this.path] || []).filter(withoutParent);
-          }.bind(this)));
-        }
-
-        return Promise.all(promises);
-      },
-
-      _computeDiffPath: function(changeNum, patchNum, path) {
-        return this.changeBaseURL(changeNum, patchNum) + '/files/' +
-            encodeURIComponent(path) + '/diff';
-      },
-
-      _computeCommentsPath: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/comments';
-      },
-
-      _computeDraftsPath: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/drafts';
-      },
-
-      _computeDiffQueryParams: function(basePatchNum) {
-        var params =  {
-          context: 'ALL',
-          intraline: null
-        };
-        if (basePatchNum != 'PARENT') {
-          params.base = basePatchNum;
-        }
-        return params;
-      },
-
-      _handlePrefsTap: function(e) {
-        e.preventDefault();
-
-        // TODO(andybons): This is not supported in IE. Implement a polyfill.
-        // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
-        // an object as a value, it must be marked enumerable.
-        this._savedPrefs = Object.assign({}, this.prefs);
-        this.$.prefsOverlay.open();
-      },
-
-      _handlePrefsSave: function(e) {
-        e.stopPropagation();
-        var el = Polymer.dom(e).rootTarget;
-        el.disabled = true;
-        app.accountReady.then(function() {
-          if (!this._loggedIn) {
-            el.disabled = false;
-            this.$.prefsOverlay.close();
-            return;
-          }
-          this._saveDiffPreferences().then(function() {
-            this.$.prefsOverlay.close();
-            el.disabled = false;
-          }.bind(this)).catch(function(err) {
-            el.disabled = false;
-            alert('Oops. Something went wrong. Check the console and bug the ' +
-                  'PolyGerrit team for assistance.');
-            throw err;
-          });
-        }.bind(this));
-      },
-
-      _saveDiffPreferences: function() {
-        var xhr = document.createElement('gr-request');
-        this._diffPreferencesPromise = xhr.send({
-          method: 'PUT',
-          url: '/accounts/self/preferences.diff',
-          body: this.prefs,
-        });
-        return this._diffPreferencesPromise;
-      },
-
-      _handlePrefsCancel: function(e) {
-        e.stopPropagation();
-        this.prefs = this._savedPrefs;
-        this.$.prefsOverlay.close();
-      },
-
-      _handleExpandContext: function(e) {
-        var ctx = e.detail.context;
-        var contextControlIndex = -1;
-        for (var i = ctx.start; i <= ctx.end; i++) {
-          this._diff.leftSide[i].hidden = false;
-          this._diff.rightSide[i].hidden = false;
-          if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
-              this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
-            contextControlIndex = i;
-          }
-        }
-        this._diff.leftSide[contextControlIndex].hidden = true;
-        this._diff.rightSide[contextControlIndex].hidden = true;
-
-        this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
-        this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
-
-        this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
-        this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
-      },
-
-      _handleThreadHeightChange: function(e) {
-        var index = e.detail.index;
-        var diffEl = Polymer.dom(e).rootTarget;
-        var otherSide = diffEl == this.$.leftDiff ?
-            this.$.rightDiff : this.$.leftDiff;
-
-        var threadHeight = e.detail.height;
-        var otherSideHeight;
-        if (otherSide.content[index].type == 'COMMENT_THREAD') {
-          otherSideHeight = otherSide.getRowNaturalHeight(index);
-        } else {
-          otherSideHeight = otherSide.getRowHeight(index);
-        }
-        var maxHeight = Math.max(threadHeight, otherSideHeight);
-        this.$.leftDiff.setRowHeight(index, maxHeight);
-        this.$.rightDiff.setRowHeight(index, maxHeight);
-      },
-
-      _handleAddDraft: function(e) {
-        var insertIndex = e.detail.index + 1;
-        var diffEl = Polymer.dom(e).rootTarget;
-        var content = diffEl.content;
-        if (content[insertIndex] &&
-            content[insertIndex].type == 'COMMENT_THREAD') {
-          // A thread is already here. Do nothing.
-          return;
-        }
-        var comment = {
-          type: 'COMMENT_THREAD',
-          comments: [{
-            __draft: true,
-            __draftID: Math.random().toString(36),
-            line: e.detail.line,
-            path: this.path,
-          }]
-        };
-        if (diffEl == this.$.leftDiff &&
-            this.patchRange.basePatchNum == 'PARENT') {
-          comment.comments[0].side = 'PARENT';
-          comment.patchNum = this.patchRange.patchNum;
-        }
-
-        if (content[insertIndex] &&
-            content[insertIndex].type == 'FILLER') {
-          content[insertIndex] = comment;
-          diffEl.rowUpdated(insertIndex);
-        } else {
-          content.splice(insertIndex, 0, comment);
-          diffEl.rowInserted(insertIndex);
-        }
-
-        var otherSide = diffEl == this.$.leftDiff ?
-            this.$.rightDiff : this.$.leftDiff;
-        if (otherSide.content[insertIndex] == null ||
-            otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
-          otherSide.content.splice(insertIndex, 0, {
-            type: 'FILLER',
-          });
-          otherSide.rowInserted(insertIndex);
-        }
-      },
-
-      _handleRemoveThread: function(e) {
-        var diffEl = Polymer.dom(e).rootTarget;
-        var otherSide = diffEl == this.$.leftDiff ?
-            this.$.rightDiff : this.$.leftDiff;
-        var index = e.detail.index;
-
-        if (otherSide.content[index].type == 'FILLER') {
-          otherSide.content.splice(index, 1);
-          otherSide.rowRemoved(index);
-          diffEl.content.splice(index, 1);
-          diffEl.rowRemoved(index);
-        } else if (otherSide.content[index].type == 'COMMENT_THREAD') {
-          diffEl.content[index] = {type: 'FILLER'};
-          diffEl.rowUpdated(index);
-          var height = otherSide.setRowNaturalHeight(index);
-          diffEl.setRowHeight(index, height);
-        } else {
-          throw Error('A thread cannot be opposite anything but filler or ' +
-              'another thread');
-        }
-      },
-
-      _processContent: function() {
-        var leftSide = [];
-        var rightSide = [];
-        var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
-        var ctx = {
-          hidingLines: false,
-          lastNumLinesHidden: 0,
-          left: {
-            lineNum: initialLineNum,
-          },
-          right: {
-            lineNum: initialLineNum,
-          }
-        };
-        var content = this._breakUpCommonChunksWithComments(ctx,
-            this._diffResponse.content);
-        var context = this.prefs.context;
-        if (context == -1) {
-          // Show the entire file.
-          context = Infinity;
-        }
-        for (var i = 0; i < content.length; i++) {
-          if (i == 0) {
-            ctx.skipRange = [0, context];
-          } else if (i == content.length - 1) {
-            ctx.skipRange = [context, 0];
-          } else {
-            ctx.skipRange = [context, context];
-          }
-          ctx.diffChunkIndex = i;
-          this._addDiffChunk(ctx, content[i], leftSide, rightSide);
-        }
-
-        this._diff = {
-          leftSide: leftSide,
-          rightSide: rightSide,
-        };
-      },
-
-      // In order to show comments out of the bounds of the selected context,
-      // treat them as diffs within the model so that the content (and context
-      // surrounding it) renders correctly.
-      _breakUpCommonChunksWithComments: function(ctx, content) {
-        var result = [];
-        var leftLineNum = ctx.left.lineNum;
-        var rightLineNum = ctx.right.lineNum;
-        for (var i = 0; i < content.length; i++) {
-          if (!content[i].ab) {
-            result.push(content[i]);
-            if (content[i].a) {
-              leftLineNum += content[i].a.length;
-            }
-            if (content[i].b) {
-              rightLineNum += content[i].b.length;
-            }
-            continue;
-          }
-          var chunk = content[i].ab;
-          var currentChunk = {ab: []};
-          for (var j = 0; j < chunk.length; j++) {
-            leftLineNum++;
-            rightLineNum++;
-            if (this._groupedBaseComments[leftLineNum] == null &&
-                this._groupedComments[rightLineNum] == null) {
-              currentChunk.ab.push(chunk[j]);
-            } else {
-              if (currentChunk.ab && currentChunk.ab.length > 0) {
-                result.push(currentChunk);
-                currentChunk = {ab: []};
-              }
-              // Append an annotation to indicate that this line should not be
-              // highlighted even though it's implied with both `a` and `b`
-              // defined. This is needed since there may be two lines that
-              // should be highlighted but are equal (blank lines, for example).
-              result.push({
-                __noHighlight: true,
-                a: [chunk[j]],
-                b: [chunk[j]],
-              });
-            }
-          }
-          if (currentChunk.ab != null && currentChunk.ab.length > 0) {
-            result.push(currentChunk);
-          }
-        }
-        return result;
-      },
-
-      _groupCommentsAndDrafts: function() {
-        this._baseDrafts.forEach(function(d) { d.__draft = true; });
-        this._drafts.forEach(function(d) { d.__draft = true; });
-        var allLeft = this._baseComments.concat(this._baseDrafts);
-        var allRight = this._comments.concat(this._drafts);
-
-        var leftByLine = {};
-        var rightByLine = {};
-        var mapFunc = function(byLine) {
-          return function(c) {
-            // File comments/drafts are grouped with line 1 for now.
-            var line = c.line || 1;
-            if (byLine[line] == null) {
-              byLine[line] = [];
-            }
-            byLine[line].push(c);
-          };
-        };
-        allLeft.forEach(mapFunc(leftByLine));
-        allRight.forEach(mapFunc(rightByLine));
-
-        this._groupedBaseComments = leftByLine;
-        this._groupedComments = rightByLine;
-      },
-
-      _addContextControl: function(ctx, leftSide, rightSide) {
-        var numLinesHidden = ctx.lastNumLinesHidden;
-        var leftStart = leftSide.length - numLinesHidden;
-        var leftEnd = leftSide.length;
-        var rightStart = rightSide.length - numLinesHidden;
-        var rightEnd = rightSide.length;
-        if (leftStart != rightStart || leftEnd != rightEnd) {
-          throw Error(
-              'Left and right ranges for context control should be equal:' +
-              'Left: [' + leftStart + ', ' + leftEnd + '] ' +
-              'Right: [' + rightStart + ', ' + rightEnd + ']');
-        }
-        var obj = {
-          type: 'CONTEXT_CONTROL',
-          numLines: numLinesHidden,
-          start: leftStart,
-          end: leftEnd,
-        };
-        // NOTE: Be careful, here. This object is meant to be immutable. If the
-        // object is altered within one side's array it will reflect the
-        // alterations in another.
-        leftSide.push(obj);
-        rightSide.push(obj);
-      },
-
-      _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-        for (var i = 0; i < chunk.ab.length; i++) {
-          var numLines = Math.ceil(
-              this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
-          var hidden = i >= ctx.skipRange[0] &&
-              i < chunk.ab.length - ctx.skipRange[1];
-          if (ctx.hidingLines && hidden == false) {
-            // No longer hiding lines. Add a context control.
-            this._addContextControl(ctx, leftSide, rightSide);
-            ctx.lastNumLinesHidden = 0;
-          }
-          ctx.hidingLines = hidden;
-          if (hidden) {
-            ctx.lastNumLinesHidden++;
-          }
-
-          // Blank lines within a diff content array indicate a newline.
-          leftSide.push({
-            type: 'CODE',
-            hidden: hidden,
-            content: chunk.ab[i] || '\n',
-            numLines: numLines,
-            lineNum: ++ctx.left.lineNum,
-          });
-          rightSide.push({
-            type: 'CODE',
-            hidden: hidden,
-            content: chunk.ab[i] || '\n',
-            numLines: numLines,
-            lineNum: ++ctx.right.lineNum,
-          });
-
-          this._addCommentsIfPresent(ctx, leftSide, rightSide);
-        }
-        if (ctx.lastNumLinesHidden > 0) {
-          this._addContextControl(ctx, leftSide, rightSide);
-        }
-      },
-
-      _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-        if (chunk.ab) {
-          this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
-          return;
-        }
-
-        var leftHighlights = [];
-        if (chunk.edit_a) {
-          leftHighlights =
-              this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
-        }
-        var rightHighlights = [];
-        if (chunk.edit_b) {
-          rightHighlights =
-              this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
-        }
-
-        var aLen = (chunk.a && chunk.a.length) || 0;
-        var bLen = (chunk.b && chunk.b.length) || 0;
-        var maxLen = Math.max(aLen, bLen);
-        for (var i = 0; i < maxLen; i++) {
-          var hasLeftContent = chunk.a && i < chunk.a.length;
-          var hasRightContent = chunk.b && i < chunk.b.length;
-          var leftContent = hasLeftContent ? chunk.a[i] : '';
-          var rightContent = hasRightContent ? chunk.b[i] : '';
-          var highlight = !chunk.__noHighlight;
-          var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
-          if (hasLeftContent) {
-            leftSide.push({
-              type: 'CODE',
-              content: leftContent || '\n',
-              numLines: maxNumLines,
-              lineNum: ++ctx.left.lineNum,
-              highlight: highlight,
-              intraline: highlight && leftHighlights.filter(function(hl) {
-                return hl.contentIndex == i;
-              }),
-            });
-          } else {
-            leftSide.push({
-              type: 'FILLER',
-              numLines: maxNumLines,
-            });
-          }
-          if (hasRightContent) {
-            rightSide.push({
-              type: 'CODE',
-              content: rightContent || '\n',
-              numLines: maxNumLines,
-              lineNum: ++ctx.right.lineNum,
-              highlight: highlight,
-              intraline: highlight && rightHighlights.filter(function(hl) {
-                return hl.contentIndex == i;
-              }),
-            });
-          } else {
-            rightSide.push({
-              type: 'FILLER',
-              numLines: maxNumLines,
-            });
-          }
-          this._addCommentsIfPresent(ctx, leftSide, rightSide);
-        }
-      },
-
-      _addCommentsIfPresent: function(ctx, leftSide, rightSide) {
-        var leftComments = this._groupedBaseComments[ctx.left.lineNum];
-        var rightComments = this._groupedComments[ctx.right.lineNum];
-        if (leftComments) {
-          var thread = {
-            type: 'COMMENT_THREAD',
-            comments: leftComments,
-          };
-          if (this.patchRange.basePatchNum == 'PARENT') {
-            thread.patchNum = this.patchRange.patchNum;
-          }
-          leftSide.push(thread);
-        }
-        if (rightComments) {
-          rightSide.push({
-            type: 'COMMENT_THREAD',
-            comments: rightComments,
-          });
-        }
-        if (leftComments && !rightComments) {
-          rightSide.push({type: 'FILLER'});
-        } else if (!leftComments && rightComments) {
-          leftSide.push({type: 'FILLER'});
-        }
-        this._groupedBaseComments[ctx.left.lineNum] = null;
-        this._groupedComments[ctx.right.lineNum] = null;
-      },
-
-      // The `highlights` array consists of a list of <skip length, mark length>
-      // pairs, where the skip length is the number of characters between the
-      // end of the previous edit and the start of this edit, and the mark
-      // length is the number of edited characters following the skip. The start
-      // of the edits is from the beginning of the related diff content lines.
-      //
-      // Note that the implied newline character at the end of each line is
-      // included in the length calculation, and thus it is possible for the
-      // edits to span newlines.
-      //
-      // A line highlight object consists of three fields:
-      // - contentIndex: The index of the diffChunk `content` field (the line
-      //   being referred to).
-      // - startIndex: Where the highlight should begin.
-      // - endIndex: (optional) Where the highlight should end. If omitted, the
-      //   highlight is meant to be a continuation onto the next line.
-      _normalizeIntralineHighlights: function(content, highlights) {
-        var contentIndex = 0;
-        var idx = 0;
-        var normalized = [];
-        for (var i = 0; i < highlights.length; i++) {
-          var line = content[contentIndex] + '\n';
-          var hl = highlights[i];
-          var j = 0;
-          while (j < hl[0]) {
-            if (idx == line.length) {
-              idx = 0;
-              line = content[++contentIndex] + '\n';
-              continue;
-            }
-            idx++;
-            j++;
-          }
-          var lineHighlight = {
-            contentIndex: contentIndex,
-            startIndex: idx,
-          };
-
-          j = 0;
-          while (line && j < hl[1]) {
-            if (idx == line.length) {
-              idx = 0;
-              line = content[++contentIndex] + '\n';
-              normalized.push(lineHighlight);
-              lineHighlight = {
-                contentIndex: contentIndex,
-                startIndex: idx,
-              };
-              continue;
-            }
-            idx++;
-            j++;
-          }
-          lineHighlight.endIndex = idx;
-          normalized.push(lineHighlight);
-        }
-        return normalized;
-      },
-
-      _visibleLineLength: function(contents) {
-        // http://jsperf.com/performance-of-match-vs-split
-        var numTabs = contents.split('\t').length - 1;
-        return contents.length - numTabs + (this.prefs.tab_size * numTabs);
-      },
-
-      _maxLinesSpanned: function(left, right) {
-        return Math.max(
-            Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
-            Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-download-dialog.html b/polygerrit-ui/app/elements/gr-download-dialog.html
deleted file mode 100644
index 3212a0f..0000000
--- a/polygerrit-ui/app/elements/gr-download-dialog.html
+++ /dev/null
@@ -1,269 +0,0 @@
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-button.html">
-
-<dom-module id="gr-download-dialog">
-  <template>
-    <style>
-      :host {
-        display: block;
-        padding: 1em;
-      }
-      ul {
-        list-style: none;
-        margin-bottom: .5em;
-      }
-      li {
-        display: inline-block;
-        margin: 0;
-        padding: 0;
-      }
-      li gr-button {
-        margin-right: 1em;
-      }
-      label,
-      input {
-        display: block;
-      }
-      label {
-        font-weight: bold;
-      }
-      input {
-        font-family: var(--monospace-font-family);
-        font-size: inherit;
-        margin-bottom: .5em;
-        width: 60em;
-      }
-      li[selected] gr-button {
-        color: #000;
-        font-weight: bold;
-        text-decoration: none;
-      }
-      header {
-        display: flex;
-        justify-content: space-between;
-      }
-      main {
-        border-bottom: 1px solid #ddd;
-        border-top: 1px solid #ddd;
-        padding: .5em;
-      }
-      footer {
-        display: flex;
-        justify-content: space-between;
-        padding-top: .75em;
-      }
-      .closeButtonContainer {
-        display: flex;
-        flex: 1;
-        justify-content: flex-end;
-      }
-      .patchFiles {
-        margin-right: 2em;
-      }
-      .patchFiles a,
-      .archives a {
-        display: inline-block;
-        margin-right: 1em;
-      }
-      .patchFiles a:last-of-type,
-      .archives a:last-of-type {
-        margin-right: 0;
-      }
-    </style>
-    <header>
-      <ul hidden$="[[!_schemes.length]]" hidden>
-        <template is="dom-repeat" items="[[_schemes]]" as="scheme">
-          <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]">
-            <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
-              [[scheme]]
-            </gr-button>
-          </li>
-        </template>
-      </ul>
-      <span class="closeButtonContainer">
-        <gr-button link on-tap="_handleCloseTap">Close</gr-button>
-      </span>
-    </header>
-    <main hidden$="[[!_schemes.length]]" hidden>
-      <template is="dom-repeat"
-          items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
-          as="command">
-        <div class="command">
-          <label>[[command.title]]</label>
-          <input is="iron-input"
-              type="text"
-              bind-value="[[command.command]]"
-              on-tap="_handleInputTap"
-              readonly>
-        </div>
-      </template>
-    </main>
-    <footer>
-      <div class="patchFiles">
-        <label>Patch file</label>
-        <div>
-          <a href$="[[_computeDownloadLink(change, patchNum)]]">
-            [[_computeDownloadFilename(change, patchNum)]]
-          </a>
-          <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
-            [[_computeZipDownloadFilename(change, patchNum)]]
-          </a>
-        </div>
-      </div>
-      <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
-        <label>Archive</label>
-        <div class="archives">
-          <template is="dom-repeat" items="[[config.archives]]" as="format">
-            <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
-              [[format]]
-            </a>
-          </template>
-        </div>
-      </div>
-    </footer>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-download-dialog',
-
-      /**
-       * Fired when the user presses the close button.
-       *
-       * @event close
-       */
-
-      properties: {
-        change: Object,
-        patchNum: String,
-        config: Object,
-
-        _schemes: {
-          type: Array,
-          value: function() { return []; },
-          computed: '_computeSchemes(change, patchNum)',
-          observer: '_schemesChanged',
-        },
-        _selectedScheme: String,
-      },
-
-      hostAttributes: {
-        role: 'dialog',
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      _computeDownloadCommands: function(change, patchNum, _selectedScheme) {
-        var commandObj;
-        for (var rev in change.revisions) {
-          if (change.revisions[rev]._number == patchNum) {
-            commandObj = change.revisions[rev].fetch[_selectedScheme].commands;
-            break;
-          }
-        }
-        var commands = [];
-        for (var title in commandObj) {
-          commands.push({
-            title: title,
-            command: commandObj[title],
-          });
-        }
-        return commands;
-      },
-
-      _computeZipDownloadLink: function(change, patchNum) {
-        return this._computeDownloadLink(change, patchNum, true);
-      },
-
-      _computeZipDownloadFilename: function(change, patchNum) {
-        return this._computeDownloadFilename(change, patchNum, true);
-      },
-
-      _computeDownloadLink: function(change, patchNum, zip) {
-        return this.changeBaseURL(change._number, patchNum) + '/patch?' +
-            (zip ? 'zip' : 'download');
-      },
-
-      _computeDownloadFilename: function(change, patchNum, zip) {
-        var shortRev;
-        for (var rev in change.revisions) {
-          if (change.revisions[rev]._number == patchNum) {
-            shortRev = rev.substr(0, 7);
-            break;
-          }
-        }
-        return shortRev + '.diff.' + (zip ? 'zip' : 'base64');
-      },
-
-      _computeArchiveDownloadLink: function(change, patchNum, format) {
-        return this.changeBaseURL(change._number, patchNum) +
-            '/archive?format=' + format;
-      },
-
-      _computeSchemes: function(change, patchNum) {
-        for (var rev in change.revisions) {
-          if (change.revisions[rev]._number == patchNum) {
-            var fetch = change.revisions[rev].fetch;
-            if (fetch) {
-              return Object.keys(fetch).sort();
-            }
-            break;
-          }
-        }
-        return [];
-      },
-
-      _computeSchemeSelected: function(scheme, selectedScheme) {
-        return scheme == selectedScheme;
-      },
-
-      _handleSchemeTap: function(e) {
-        e.preventDefault();
-        var el = Polymer.dom(e).rootTarget;
-        // TODO(andybons): Save as default scheme in preferences.
-        this._selectedScheme = el.getAttribute('data-scheme');
-      },
-
-      _handleInputTap: function(e) {
-        e.preventDefault();
-        Polymer.dom(e).rootTarget.select();
-      },
-
-      _handleCloseTap: function(e) {
-        e.preventDefault();
-        this.fire('close', null, {bubbles: false});
-      },
-
-      _schemesChanged: function(schemes) {
-        if (schemes.length == 0) { return; }
-        if (schemes.indexOf(this._selectedScheme) == -1) {
-          this._selectedScheme = schemes.sort()[0];
-        }
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html
deleted file mode 100644
index bcf3e05..0000000
--- a/polygerrit-ui/app/elements/gr-file-list.html
+++ /dev/null
@@ -1,352 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-file-list">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      .row {
-        display: flex;
-        padding: .1em .25em;
-      }
-      .header {
-        font-weight: bold;
-      }
-      .positionIndicator,
-      .reviewed,
-      .status {
-        align-items: center;
-        display: inline-flex;
-      }
-      .reviewed,
-      .status {
-        justify-content: center;
-        width: 1.5em;
-      }
-      .positionIndicator {
-        justify-content: flex-start;
-        visibility: hidden;
-        width: 1.25em;
-      }
-      .row[selected] {
-        background-color: #ebf5fb;
-      }
-      .row[selected] .positionIndicator {
-        visibility: visible;
-      }
-      .path {
-        flex: 1;
-        overflow: hidden;
-        padding-left: .35em;
-        text-decoration: none;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
-      .row:not(.header) .path:hover {
-        text-decoration: underline;
-      }
-      .comments,
-      .stats {
-        text-align: right;
-      }
-      .comments {
-        min-width: 10em;
-      }
-      .stats {
-        min-width: 7em;
-      }
-      .invisible {
-        visibility: hidden;
-      }
-      .row:not(.header) .stats {
-        font-family: var(--monospace-font-family);
-      }
-      .added {
-        color: #388E3C;
-      }
-      .removed {
-        color: #D32F2F;
-      }
-      .reviewed input[type="checkbox"] {
-        display: inline-block;
-      }
-      .drafts {
-        color: #C62828;
-        font-weight: bold;
-      }
-      @media screen and (max-width: 50em) {
-        .row[selected] {
-          background-color: transparent;
-        }
-        .positionIndicator,
-        .stats {
-          display: none;
-        }
-        .reviewed,
-        .status {
-          justify-content: flex-start;
-        }
-        .comments {
-          min-width: initial;
-        }
-      }
-    </style>
-    <gr-ajax id="filesXHR"
-        url="[[_computeFilesURL(changeNum, patchNum)]]"
-        on-response="_handleResponse"></gr-ajax>
-    <gr-ajax id="draftsXHR"
-        url="[[_computeDraftsURL(changeNum, patchNum)]]"
-        last-response="{{_drafts}}"></gr-ajax>
-    <gr-ajax id="reviewedXHR"
-        url="[[_computeReviewedURL(changeNum, patchNum)]]"
-        last-response="{{_reviewed}}"></gr-ajax>
-    </gr-ajax>
-
-    <div class="row header">
-      <div class="positionIndicator"></div>
-      <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
-      <div class="status"></div>
-      <div class="path">Path</div>
-      <div class="comments">Comments</div>
-      <div class="stats">Stats</div>
-    </div>
-    <template is="dom-repeat" items="{{files}}" as="file">
-      <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
-        <div class="positionIndicator">&#x25b6;</div>
-        <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
-          <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
-              data-path$="[[file.__path]]" on-change="_handleReviewedChange">
-        </div>
-        <div class$="[[_computeClass('status', file.__path)]]">
-          [[_computeFileStatus(file.status)]]
-        </div>
-        <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
-          [[_computeFileDisplayName(file.__path)]]
-        </a>
-        <div class="comments">
-          <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
-          [[_computeCommentsString(comments, patchNum, file.__path)]]
-        </div>
-        <div class$="[[_computeClass('stats', file.__path)]]">
-          <span class="added">+[[file.lines_inserted]]</span>
-          <span class="removed">-[[file.lines_deleted]]</span>
-        </div>
-      </div>
-    </template>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-
-    Polymer({
-      is: 'gr-file-list',
-
-      properties: {
-        patchNum: String,
-        changeNum: String,
-        comments: Object,
-        files: Array,
-        selectedIndex: {
-          type: Number,
-          notify: true,
-        },
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-
-        _loggedIn: {
-          type: Boolean,
-          value: false,
-        },
-        _drafts: Object,
-        _reviewed: {
-          type: Array,
-          value: function() { return []; },
-        },
-        _xhrPromise: Object,  // Used for testing.
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-        Gerrit.RESTClientBehavior,
-      ],
-
-      reload: function() {
-        if (!this.changeNum || !this.patchNum) {
-          return Promise.resolve();
-        }
-        return Promise.all([
-          this.$.filesXHR.generateRequest().completes,
-          app.accountReady.then(function() {
-            this._loggedIn = app.loggedIn;
-            if (!app.loggedIn) { return; }
-            this.$.draftsXHR.generateRequest();
-            this.$.reviewedXHR.generateRequest();
-          }.bind(this)),
-        ]);
-      },
-
-      _computeFilesURL: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/files';
-      },
-
-      _computeCommentsString: function(comments, patchNum, path) {
-        var patchComments = (comments[path] || []).filter(function(c) {
-          return c.patch_set == patchNum;
-        });
-        var num = patchComments.length;
-        if (num == 0) { return ''; }
-        if (num == 1) { return '1 comment'; }
-        if (num > 1) { return num + ' comments'; }
-      },
-
-      _computeReviewedURL: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
-      },
-
-      _computeReviewed: function(file, _reviewed) {
-        return _reviewed.indexOf(file.__path) != -1;
-      },
-
-      _handleReviewedChange: function(e) {
-        var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
-        var index = this._reviewed.indexOf(path);
-        var reviewed = index != -1;
-        if (reviewed) {
-          this.splice('_reviewed', index, 1);
-        } else {
-          this.push('_reviewed', path);
-        }
-
-        var method = reviewed ? 'DELETE' : 'PUT';
-        var url = this.changeBaseURL(this.changeNum, this.patchNum) +
-            '/files/' + encodeURIComponent(path) + '/reviewed';
-        this._send(method, url).catch(function(err) {
-          alert('Couldn’t change file review status. Check the console ' +
-              'and contact the PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
-      },
-
-      _computeDraftsURL: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/drafts';
-      },
-
-      _computeDraftsString: function(drafts, path) {
-        var num = (drafts[path] || []).length;
-        if (num == 0) { return ''; }
-        if (num == 1) { return '1 draft'; }
-        if (num > 1) { return num + ' drafts'; }
-      },
-
-      _handleResponse: function(e, req) {
-        var result = e.detail.response;
-        var paths = Object.keys(result).sort();
-        var files = [];
-        for (var i = 0; i < paths.length; i++) {
-          var info = result[paths[i]];
-          info.__path = paths[i];
-          info.lines_inserted = info.lines_inserted || 0;
-          info.lines_deleted = info.lines_deleted || 0;
-          files.push(info);
-        }
-        this.files = files;
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
-        switch (e.keyCode) {
-          case 74:  // 'j'
-            e.preventDefault();
-            this.selectedIndex =
-                Math.min(this.files.length - 1, this.selectedIndex + 1);
-            break;
-          case 75:  // 'k'
-            e.preventDefault();
-            this.selectedIndex = Math.max(0, this.selectedIndex - 1);
-            break;
-          case 219:  // '['
-            e.preventDefault();
-            this._openSelectedFile(this.files.length - 1);
-            break;
-          case 221:  // ']'
-            e.preventDefault();
-            this._openSelectedFile(0);
-            break;
-          case 13:  // <enter>
-          case 79:  // 'o'
-            e.preventDefault();
-            this._openSelectedFile();
-            break;
-        }
-      },
-
-      _openSelectedFile: function(opt_index) {
-        if (opt_index != null) {
-          this.selectedIndex = opt_index;
-        }
-        page.show(this._computeDiffURL(this.changeNum, this.patchNum,
-            this.files[this.selectedIndex].__path));
-      },
-
-      _computeFileSelected: function(index, selectedIndex) {
-        return index == selectedIndex;
-      },
-
-      _computeFileStatus: function(status) {
-        return status || 'M';
-      },
-
-      _computeDiffURL: function(changeNum, patchNum, path) {
-        return '/c/' + changeNum + '/' + patchNum + '/' + path;
-      },
-
-      _computeFileDisplayName: function(path) {
-        return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
-      },
-
-      _computeClass: function(baseClass, path) {
-        var classes = [baseClass];
-        if (path == COMMIT_MESSAGE_PATH) {
-          classes.push('invisible');
-        }
-        return classes.join(' ');
-      },
-
-      _send: function(method, url) {
-        var xhr = document.createElement('gr-request');
-        this._xhrPromise = xhr.send({
-          method: method,
-          url: url,
-        });
-        return this._xhrPromise;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-linked-text.html b/polygerrit-ui/app/elements/gr-linked-text.html
deleted file mode 100644
index 5c2d9a5..0000000
--- a/polygerrit-ui/app/elements/gr-linked-text.html
+++ /dev/null
@@ -1,101 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<script src="../scripts/ba-linkify.js"></script>
-<script src="../scripts/link-text-parser.js"></script>
-<dom-module id="gr-linked-text">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      :host([pre]) span {
-        white-space: pre-wrap;
-        word-wrap: break-word;
-      }
-      :host([disabled]) a {
-        color: inherit;
-        text-decoration: none;
-        pointer-events: none;
-      }
-    </style>
-    <span id="output"></span>
-  </template>
-  <script>
-    'use strict';
-
-    Polymer({
-      is: 'gr-linked-text',
-
-      properties: {
-        content: {
-          type: String,
-          observer: '_contentChanged',
-        },
-        pre: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        config: Object,
-      },
-
-      observers: [
-        '_contentOrConfigChanged(content, config)',
-      ],
-
-      _contentChanged: function(content) {
-        // In the case where the config may not be set (perhaps due to the
-        // request for it still being in flight), set the content anyway to
-        // prevent waiting on the config to display the text.
-        if (this.config != null) { return; }
-        this.$.output.textContent = content;
-      },
-
-      _contentOrConfigChanged: function(content, config) {
-        var output = Polymer.dom(this.$.output);
-        output.textContent = '';
-        var parser = new GrLinkTextParser(config, function(text, href, html) {
-          if (href) {
-            var a = document.createElement('a');
-            a.href = href;
-            a.textContent = text;
-            a.target = '_blank';
-            output.appendChild(a);
-          } else if (html) {
-            var fragment = document.createDocumentFragment();
-            // Create temporary div to hold the nodes in.
-            var div = document.createElement('div');
-            div.innerHTML = html;
-            while (div.firstChild) {
-              fragment.appendChild(div.firstChild);
-            }
-            output.appendChild(fragment);
-          } else {
-            output.appendChild(document.createTextNode(text));
-          }
-        });
-        parser.parse(content);
-      }
-    });
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
deleted file mode 100644
index fef8968..0000000
--- a/polygerrit-ui/app/elements/gr-message.html
+++ /dev/null
@@ -1,223 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-account-link.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-comment-list.html">
-<link rel="import" href="gr-date-formatter.html">
-<link rel="import" href="gr-linked-text.html">
-
-<dom-module id="gr-message">
-  <template>
-    <style>
-      :host {
-        border-top: 1px solid #ddd;
-        display: block;
-        position: relative;
-      }
-      :host(:not([expanded])) {
-        cursor: pointer;
-      }
-      gr-avatar {
-        position: absolute;
-        left: var(--default-horizontal-margin);
-      }
-      .collapsed .contentContainer {
-        color: #777;
-        white-space: nowrap;
-        overflow-x: hidden;
-        text-overflow: ellipsis;
-      }
-      .showAvatar.expanded .contentContainer {
-        margin-left: calc(var(--default-horizontal-margin) + 2.5em);
-        padding: 10px 0;
-      }
-      .showAvatar.collapsed .contentContainer {
-        margin-left: calc(var(--default-horizontal-margin) + 1.75em);
-        padding: 10px 75px 10px 0;
-      }
-      .hideAvatar.collapsed .contentContainer,
-      .hideAvatar.expanded .contentContainer {
-        margin-left: 0;
-        padding: 10px 75px 10px 0;
-      }
-      .collapsed gr-avatar {
-        top: 8px;
-        height: 1.75em;
-        width: 1.75em;
-      }
-      .expanded gr-avatar {
-        top: 12px;
-        height: 2.5em;
-        width: 2.5em;
-      }
-      .name {
-        font-weight: bold;
-      }
-      .content {
-        font-family: var(--monospace-font-family);
-      }
-      .collapsed .name,
-      .collapsed .content,
-      .collapsed .message {
-        display: inline;
-      }
-      .collapsed gr-comment-list,
-      .collapsed .replyContainer {
-        display: none;
-      }
-      .collapsed .name {
-        color: var(--default-text-color);
-      }
-      .expanded .name {
-        cursor: pointer;
-      }
-      .date {
-        color: #666;
-        position: absolute;
-        right: var(--default-horizontal-margin);
-        top: 10px;
-      }
-      .replyContainer {
-        padding: .5em 0 1em;
-      }
-    </style>
-    <div class$="[[_computeClass(expanded, showAvatar)]]">
-      <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
-      <div class="contentContainer">
-        <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div>
-        <div class="content">
-          <gr-linked-text class="message"
-              pre="[[expanded]]"
-              content="[[message.message]]"
-              disabled="[[!expanded]]"
-              config="[[projectConfig.commentlinks]]"></gr-linked-text>
-          <gr-comment-list
-              comments="[[comments]]"
-              change-num="[[changeNum]]"
-              patch-num="[[message._revision_number]]"></gr-comment-list>
-        </div>
-        <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
-          <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
-        </a>
-      </div>
-      <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
-        <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
-      </div>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-message',
-
-      /**
-       * Fired when this message's permalink is tapped.
-       *
-       * @event scroll-to
-       */
-
-      /**
-       * Fired when this message's reply link is tapped.
-       *
-       * @event reply
-       */
-
-      listeners: {
-        'tap': '_handleTap',
-      },
-
-      properties: {
-        changeNum: Number,
-        message: Object,
-        comments: {
-          type: Object,
-          observer: '_commentsChanged',
-        },
-        expanded: {
-          type: Boolean,
-          value: true,
-          reflectToAttribute: true,
-        },
-        showAvatar: {
-          type: Boolean,
-          value: false,
-        },
-        showReplyButton: {
-          type: Boolean,
-          value: false,
-        },
-        projectConfig: Object,
-      },
-
-      ready: function() {
-        app.configReady.then(function(cfg) {
-          this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
-              this.message && this.message.author;
-        }.bind(this));
-      },
-
-      _commentsChanged: function(value) {
-        this.expanded = Object.keys(value || {}).length > 0;
-      },
-
-      _handleTap: function(e) {
-        if (this.expanded) { return; }
-        this.expanded = true;
-      },
-
-      _handleNameTap: function(e) {
-        if (!this.expanded) { return; }
-        e.stopPropagation();
-        this.expanded = false;
-      },
-
-      _computeClass: function(expanded, showAvatar) {
-        var classes = [];
-        classes.push(expanded ? 'expanded' : 'collapsed');
-        classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
-        return classes.join(' ');
-      },
-
-      _computeMessageHash: function(message) {
-        return '#message-' + message.id;
-      },
-
-      _handleLinkTap: function(e) {
-        e.preventDefault();
-
-        this.fire('scroll-to', {message: this.message}, {bubbles: false});
-
-        var hash = this._computeMessageHash(this.message);
-        // Don't add the hash to the window history if it's already there.
-        // Otherwise you mess up expected back button behavior.
-        if (window.location.hash == hash) { return; }
-        // Change the URL but don’t trigger a nav event. Otherwise it will
-        // reload the page.
-        page.show(window.location.pathname + hash, null, false);
-      },
-
-      _handleReplyTap: function(e) {
-        e.preventDefault();
-        this.fire('reply', {message: this.message});
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-messages-list.html b/polygerrit-ui/app/elements/gr-messages-list.html
deleted file mode 100644
index 38bec1a..0000000
--- a/polygerrit-ui/app/elements/gr-messages-list.html
+++ /dev/null
@@ -1,162 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-message.html">
-
-<dom-module id="gr-messages-list">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      .header {
-        display: flex;
-        justify-content: space-between;
-        margin-bottom: .35em;
-      }
-      .header,
-      gr-message {
-        padding: 0 var(--default-horizontal-margin);
-      }
-      .highlighted {
-        animation: 3s fadeOut;
-      }
-      @keyframes fadeOut {
-        0% { background-color: #fff9c4; }
-        100% { background-color: #fff; }
-      }
-    </style>
-    <div class="header">
-      <h3>Messages</h3>
-      <gr-button link on-tap="_handleExpandCollapseTap">
-        [[_computeExpandCollapseMessage(_expanded)]]
-      </gr-button>
-    </div>
-    <template is="dom-repeat" items="[[messages]]" as="message">
-      <gr-message
-          change-num="[[changeNum]]"
-          message="[[message]]"
-          comments="[[_computeCommentsForMessage(comments, message, index)]]"
-          project-config="[[projectConfig]]"
-          show-reply-button="[[showReplyButtons]]"
-          on-scroll-to="_handleScrollTo"
-          data-message-id$="[[message.id]]"></gr-message>
-    </template>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-messages-list',
-
-      properties: {
-        changeNum: Number,
-        messages: {
-          type: Array,
-          value: function() { return []; },
-        },
-        comments: Object,
-        projectConfig: Object,
-        topMargin: Number,
-        showReplyButtons: {
-          type: Boolean,
-          value: false,
-        },
-
-        _expanded: {
-          type: Boolean,
-          value: false,
-        },
-      },
-
-      scrollToMessage: function(messageID) {
-        var el = this.$$('[data-message-id="' + messageID + '"]');
-        if (!el) { return; }
-
-        el.expanded = true;
-        var top = el.offsetTop;
-        for (var offsetParent = el.offsetParent;
-             offsetParent;
-             offsetParent = offsetParent.offsetParent) {
-          top += offsetParent.offsetTop;
-        }
-        window.scrollTo(0, top - this.topMargin);
-        this._highlightEl(el);
-      },
-
-      _highlightEl: function(el) {
-        var highlightedEls =
-            Polymer.dom(this.root).querySelectorAll('.highlighted');
-        for (var i = 0; i < highlightedEls.length; i++) {
-          highlightedEls[i].classList.remove('highlighted');
-        }
-        function handleAnimationEnd() {
-          el.removeEventListener('animationend', handleAnimationEnd);
-          el.classList.remove('highlighted');
-        }
-        el.addEventListener('animationend', handleAnimationEnd);
-        el.classList.add('highlighted');
-      },
-
-      _handleExpandCollapseTap: function(e) {
-        e.preventDefault();
-        this._expanded = !this._expanded;
-        var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message');
-        for (var i = 0; i < messageEls.length; i++) {
-          messageEls[i].expanded = this._expanded;
-        }
-      },
-
-      _handleScrollTo: function(e) {
-        this.scrollToMessage(e.detail.message.id);
-      },
-
-      _computeExpandCollapseMessage: function(expanded) {
-        return expanded ? 'Collapse all' : 'Expand all';
-      },
-
-      _computeCommentsForMessage: function(comments, message, index) {
-        comments = comments || {};
-        var messages = this.messages || [];
-        var msgComments = {};
-        var mDate = util.parseDate(message.date);
-        var nextMDate;
-        if (index < messages.length - 1) {
-          nextMDate = util.parseDate(messages[index + 1].date);
-        }
-        for (var file in comments) {
-          var fileComments = comments[file];
-          for (var i = 0; i < fileComments.length; i++) {
-            var cDate = util.parseDate(fileComments[i].updated);
-            if (cDate >= mDate) {
-              if (nextMDate && cDate >= nextMDate) {
-                continue;
-              }
-              msgComments[file] = msgComments[file] || [];
-              msgComments[file].push(fileComments[i]);
-            }
-          }
-        }
-        return msgComments;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-overlay.html b/polygerrit-ui/app/elements/gr-overlay.html
deleted file mode 100644
index 3119747..0000000
--- a/polygerrit-ui/app/elements/gr-overlay.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-
-<dom-module id="gr-overlay">
-  <template>
-    <style>
-      :host {
-        background: #fff;
-        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
-      }
-    </style>
-    <content></content>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-overlay',
-
-      behaviors: [
-        Polymer.IronOverlayBehavior,
-      ],
-
-      detached: function() {
-        // For good measure.
-        Gerrit.KeyboardShortcutBehavior.enabled = true;
-      },
-
-      open: function() {
-        Gerrit.KeyboardShortcutBehavior.enabled = false;
-        Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments);
-      },
-
-      close: function() {
-        Gerrit.KeyboardShortcutBehavior.enabled = true;
-        Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments);
-      },
-
-      cancel: function() {
-        Gerrit.KeyboardShortcutBehavior.enabled = true;
-        Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments);
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-patch-range-select.html b/polygerrit-ui/app/elements/gr-patch-range-select.html
deleted file mode 100644
index a2a5dc4..0000000
--- a/polygerrit-ui/app/elements/gr-patch-range-select.html
+++ /dev/null
@@ -1,95 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-
-<dom-module id="gr-patch-range-select">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      .patchRange {
-        display: inline-block;
-      }
-    </style>
-    Patch set:
-    <span class="patchRange">
-      <select id="leftPatchSelect" on-change="_handlePatchChange">
-        <option value="PARENT"
-            selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
-        <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
-          <option value$="[[patchNum]]"
-              selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
-              disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
-        </template>
-      </select>
-    </span>
-    &rarr;
-    <span class="patchRange">
-      <select id="rightPatchSelect" on-change="_handlePatchChange">
-        <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
-          <option value$="[[patchNum]]"
-              selected$="[[_computeRightSelected(patchNum, patchRange)]]"
-              disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
-        </template>
-      </select>
-    </span>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-patch-range-select',
-
-      properties: {
-        availablePatches: Array,
-        changeNum: String,
-        patchRange: Object,
-        path: String,
-      },
-
-      _handlePatchChange: function(e) {
-        var leftPatch = this.$.leftPatchSelect.value;
-        var rightPatch = this.$.rightPatchSelect.value;
-        var rangeStr = rightPatch;
-        if (leftPatch != 'PARENT') {
-          rangeStr = leftPatch + '..' + rangeStr;
-        }
-        page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
-      },
-
-      _computeLeftSelected: function(patchNum, patchRange) {
-        return patchNum == patchRange.basePatchNum;
-      },
-
-      _computeRightSelected: function(patchNum, patchRange) {
-        return patchNum == patchRange.patchNum;
-      },
-
-      _computeLeftDisabled: function(patchNum, patchRange) {
-        return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
-      },
-
-      _computeRightDisabled: function(patchNum, patchRange) {
-        if (patchRange.basePatchNum == 'PARENT') { return false; }
-        return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-related-changes-list.html b/polygerrit-ui/app/elements/gr-related-changes-list.html
deleted file mode 100644
index e3a7d2e..0000000
--- a/polygerrit-ui/app/elements/gr-related-changes-list.html
+++ /dev/null
@@ -1,363 +0,0 @@
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-
-<dom-module id="gr-related-changes-list">
-  <template>
-    <style>
-      :host {
-        display: block;
-      }
-      h3 {
-        margin: .5em 0 0;
-      }
-      section {
-        margin-bottom: 1em;
-      }
-      a {
-        display: block;
-      }
-      .relatedChanges a {
-        display: inline-block;
-      }
-      .strikethrough {
-        color: #666;
-        text-decoration: line-through;
-      }
-      .status {
-        color: #666;
-        font-weight: bold;
-      }
-      .notCurrent {
-        color: #e65100;
-      }
-      .indirectAncestor {
-        color: #33691e;
-      }
-      .submittable {
-        color: #1b5e20;
-      }
-      .hidden {
-        display: none;
-      }
-    </style>
-    <gr-ajax id="relatedXHR"
-        url="[[_computeRelatedURL(change._number, patchNum)]]"
-        last-response="{{_relatedResponse}}"></gr-ajax>
-    <gr-ajax id="submittedTogetherXHR"
-        url="[[_computeSubmittedTogetherURL(change._number)]]"
-        last-response="{{_submittedTogether}}"></gr-ajax>
-    <gr-ajax id="conflictsXHR"
-        url="/changes/"
-        params="[[_computeConflictsQueryParams(change._number)]]"
-        last-response="{{_conflicts}}"></gr-ajax>
-    <gr-ajax id="cherryPicksXHR"
-        url="/changes/"
-        params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]"
-        last-response="{{_cherryPicks}}"></gr-ajax>
-    <gr-ajax id="sameTopicXHR"
-        url="/changes/"
-        params="[[_computeSameTopicQueryParams(change.topic)]]"
-        last-response="{{_sameTopic}}"></gr-ajax>
-
-    <div hidden$="[[!_loading]]">Loading...</div>
-    <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
-      <h4>Relation Chain</h4>
-      <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change">
-        <div>
-          <a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]"
-              class$="[[_computeLinkClass(change)]]">
-            [[change.commit.subject]]
-          </a>
-          <span class$="[[_computeChangeStatusClass(change)]]">
-            ([[_computeChangeStatus(change)]])
-          </span>
-        </div>
-      </template>
-    </section>
-    <section hidden$="[[!_submittedTogether.length]]" hidden>
-      <h4>Submitted together</h4>
-      <template is="dom-repeat" items="[[_submittedTogether]]" as="change">
-        <a href$="[[_computeChangeURL(change._number)]]"
-            class$="[[_computeLinkClass(change)]]">
-          [[change.project]]: [[change.branch]]: [[change.subject]]
-        </a>
-      </template>
-    </section>
-    <section hidden$="[[!_sameTopic.length]]" hidden>
-      <h4>Same topic</h4>
-      <template is="dom-repeat" items="[[_sameTopic]]" as="change">
-        <a href$="[[_computeChangeURL(change._number)]]"
-            class$="[[_computeLinkClass(change)]]">
-          [[change.project]]: [[change.branch]]: [[change.subject]]
-        </a>
-      </template>
-    </section>
-    <section hidden$="[[!_conflicts.length]]" hidden>
-      <h4>Merge conflicts</h4>
-      <template is="dom-repeat" items="[[_conflicts]]" as="change">
-        <a href$="[[_computeChangeURL(change._number)]]"
-            class$="[[_computeLinkClass(change)]]">
-          [[change.subject]]
-        </a>
-      </template>
-    </section>
-    <section hidden$="[[!_cherryPicks.length]]" hidden>
-      <h4>Cherry picks</h4>
-      <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
-        <a href$="[[_computeChangeURL(change._number)]]"
-            class$="[[_computeLinkClass(change)]]">
-          [[change.subject]]
-        </a>
-      </template>
-    </section>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-related-changes-list',
-
-      properties: {
-        change: Object,
-        patchNum: String,
-        serverConfig: {
-          type: Object,
-          observer: '_serverConfigChanged',
-        },
-        hidden: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-
-        _loading: Boolean,
-        _resolveServerConfigReady: Function,
-        _serverConfigReady: {
-          type: Object,
-          value: function() {
-            return new Promise(function(resolve) {
-              this._resolveServerConfigReady = resolve;
-            }.bind(this));
-          }
-        },
-        _connectedRevisions: {
-          type: Array,
-          computed: '_computeConnectedRevisions(change, patchNum, ' +
-              '_relatedResponse.changes)',
-        },
-        _relatedResponse: Object,
-        _submittedTogether: Array,
-        _conflicts: Array,
-        _cherryPicks: Array,
-        _sameTopic: Array,
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      observers: [
-        '_resultsChanged(_relatedResponse.changes, _submittedTogether, ' +
-            '_conflicts, _cherryPicks, _sameTopic)',
-      ],
-
-      reload: function() {
-        if (!this.change || !this.patchNum) {
-          return Promise.resolve();
-        }
-        this._loading = true;
-        var promises = [
-          this.$.relatedXHR.generateRequest().completes,
-          this.$.submittedTogetherXHR.generateRequest().completes,
-          this.$.conflictsXHR.generateRequest().completes,
-          this.$.cherryPicksXHR.generateRequest().completes,
-        ];
-
-        return this._serverConfigReady.then(function() {
-          if (this.change.topic &&
-              !this.serverConfig.change.submit_whole_topic) {
-            return this.$.sameTopicXHR.generateRequest().completes;
-          } else {
-            this._sameTopic = [];
-          }
-          return Promise.resolve();
-        }.bind(this)).then(Promise.all(promises)).then(function() {
-          this._loading = false;
-        }.bind(this));
-      },
-
-      _computeRelatedURL: function(changeNum, patchNum) {
-        return this.changeBaseURL(changeNum, patchNum) + '/related';
-      },
-
-      _computeSubmittedTogetherURL: function(changeNum) {
-        return this.changeBaseURL(changeNum) + '/submitted_together';
-      },
-
-      _computeConflictsQueryParams: function(changeNum) {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.CURRENT_REVISION,
-            this.ListChangesOption.CURRENT_COMMIT
-        );
-        return {
-          O: options,
-          q: 'status:open is:mergeable conflicts:' + changeNum,
-        };
-      },
-
-      _computeCherryPicksQueryParams: function(project, changeID, changeNum) {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.CURRENT_REVISION,
-            this.ListChangesOption.CURRENT_COMMIT
-        );
-        var query = [
-          'project:' + project,
-          'change:' + changeID,
-          '-change:' + changeNum,
-          '-is:abandoned',
-        ].join(' ');
-        return {
-          O: options,
-          q: query
-        }
-      },
-
-      _computeSameTopicQueryParams: function(topic) {
-        var options = this.listChangesOptionsToHex(
-            this.ListChangesOption.LABELS,
-            this.ListChangesOption.CURRENT_REVISION,
-            this.ListChangesOption.CURRENT_COMMIT,
-            this.ListChangesOption.DETAILED_LABELS
-        );
-        return {
-          O: options,
-          q: 'status:open topic:' + topic,
-        };
-      },
-
-      _computeChangeURL: function(changeNum, patchNum) {
-        var urlStr = '/c/' + changeNum;
-        if (patchNum != null) {
-          urlStr += '/' + patchNum;
-        }
-        return urlStr;
-      },
-
-      _computeLinkClass: function(change) {
-        if (change.status == this.ChangeStatus.ABANDONED) {
-          return 'strikethrough';
-        }
-      },
-
-      _computeChangeStatusClass: function(change) {
-        var classes = ['status'];
-        if (change._revision_number != change._current_revision_number) {
-          classes.push('notCurrent');
-        } else if (this._isIndirectAncestor(change)) {
-          classes.push('indirectAncestor');
-        } else if (change.submittable) {
-          classes.push('submittable');
-        } else if (change.status == this.ChangeStatus.NEW) {
-          classes.push('hidden');
-        }
-        return classes.join(' ');
-      },
-
-      _computeChangeStatus: function(change) {
-        switch (change.status) {
-          case this.ChangeStatus.MERGED:
-            return 'Merged';
-          case this.ChangeStatus.ABANDONED:
-            return 'Abandoned';
-          case this.ChangeStatus.DRAFT:
-            return 'Draft';
-        }
-        if (change._revision_number != change._current_revision_number) {
-          return 'Not current';
-        } else if (this._isIndirectAncestor(change)) {
-          return 'Indirect ancestor';
-        } else if (change.submittable) {
-          return 'Submittable';
-        }
-        return ''
-      },
-
-      _serverConfigChanged: function(config) {
-        this._resolveServerConfigReady(config);
-      },
-
-      _resultsChanged: function(related, submittedTogether, conflicts,
-          cherryPicks, sameTopic) {
-        var results = [
-          related,
-          submittedTogether,
-          conflicts,
-          cherryPicks,
-          sameTopic
-        ];
-        for (var i = 0; i < results.length; i++) {
-          if (results[i].length > 0) {
-            this.hidden = false;
-            return;
-          }
-        }
-        this.hidden = true;
-      },
-
-      _isIndirectAncestor: function(change) {
-        return this._connectedRevisions.indexOf(change.commit.commit) == -1;
-      },
-
-      _computeConnectedRevisions: function(change, patchNum, relatedChanges) {
-        var connected = [];
-        var changeRevision;
-        for (var rev in change.revisions) {
-          if (change.revisions[rev]._number == patchNum) {
-            changeRevision = rev;
-          }
-        }
-        var commits = relatedChanges.map(function(c) { return c.commit; });
-        var pos = commits.length - 1;
-
-        while (pos >= 0) {
-          var commit = commits[pos].commit;
-          connected.push(commit);
-          if (commit == changeRevision) {
-            break;
-          }
-          pos--;
-        }
-        while (pos >= 0) {
-          for (var i = 0; i < commits[pos].parents.length; i++) {
-            if (connected.indexOf(commits[pos].parents[i].commit) != -1) {
-              connected.push(commits[pos].commit);
-              break;
-            }
-          }
-          --pos;
-        }
-        return connected;
-      },
-
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reply-dialog.html b/polygerrit-ui/app/elements/gr-reply-dialog.html
deleted file mode 100644
index ead001a..0000000
--- a/polygerrit-ui/app/elements/gr-reply-dialog.html
+++ /dev/null
@@ -1,307 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
-<link rel="import" href="../behaviors/rest-client-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-reply-dialog">
-  <style>
-    :host {
-      display: block;
-      max-height: 90vh;
-    }
-    :host([disabled]) {
-      pointer-events: none;
-    }
-    :host([disabled]) .container {
-      opacity: .5;
-    }
-    .container {
-      display: flex;
-      flex-direction: column;
-      max-height: 90vh;
-    }
-    section {
-      border-top: 1px solid #ddd;
-      padding: .5em .75em;
-    }
-    .textareaContainer,
-    .labelsContainer,
-    .actionsContainer {
-      flex-shrink: 0;
-    }
-    .textareaContainer {
-      position: relative;
-    }
-    iron-autogrow-textarea {
-      padding: 0;
-      font-family: var(--monospace-font-family);
-    }
-    .message {
-      border: none;
-      width: 100%;
-    }
-    .labelContainer:not(:first-of-type) {
-      margin-top: .5em;
-    }
-    .labelName {
-      display: inline-block;
-      width: 7em;
-      margin-right: .5em;
-      white-space: nowrap;
-    }
-    iron-selector {
-      display: inline-flex;
-    }
-    iron-selector > gr-button {
-      margin-right: .25em;
-    }
-    iron-selector > gr-button:first-of-type {
-      border-top-left-radius: 2px;
-      border-bottom-left-radius: 2px;
-    }
-    iron-selector > gr-button:last-of-type {
-      border-top-right-radius: 2px;
-      border-bottom-right-radius: 2px;
-    }
-    iron-selector > gr-button.iron-selected {
-      background-color: #ddd;
-    }
-    .draftsContainer {
-      overflow-y: auto;
-    }
-    .draftsContainer h3 {
-      margin-top: .25em;
-    }
-    .actionsContainer {
-      display: flex;
-      justify-content: space-between;
-    }
-    .action:link,
-    .action:visited {
-      color: #00e;
-    }
-  </style>
-  <template>
-    <gr-ajax id="draftsXHR"
-        url="[[_computeDraftsURL(changeNum)]]"
-        last-response="{{_drafts}}"></gr-ajax>
-    <div class="container">
-      <section class="textareaContainer">
-        <iron-autogrow-textarea
-            id="textarea"
-            class="message"
-            placeholder="Say something..."
-            disabled="{{disabled}}"
-            rows="4"
-            max-rows="15"
-            bind-value="{{draft}}"></iron-autogrow-textarea>
-      </section>
-      <section class="labelsContainer">
-        <template is="dom-repeat"
-            items="[[_computeLabelArray(permittedLabels)]]" as="label">
-          <div class="labelContainer">
-            <span class="labelName">[[label]]</span>
-            <iron-selector data-label$="[[label]]"
-                selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
-              <template is="dom-repeat"
-                  items="[[_computePermittedLabelValues(permittedLabels, label)]]"
-                  as="value">
-                <gr-button data-value$="[[value]]">[[value]]</gr-button>
-              </template>
-            </iron-selector>
-          </div>
-        </template>
-      </section>
-      <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
-        <h3>[[_computeDraftsTitle(_drafts)]]</h3>
-        <gr-comment-list
-            comments="[[_drafts]]"
-            change-num="[[changeNum]]"
-            patch-num="[[patchNum]]"></gr-comment-list>
-      </section>
-      <section class="actionsContainer">
-        <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
-        <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
-      </section>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-reply-dialog',
-
-      /**
-       * Fired when a reply is successfully sent.
-       *
-       * @event send
-       */
-
-      /**
-       * Fired when the user presses the cancel button.
-       *
-       * @event cancel
-       */
-
-      properties: {
-        changeNum: String,
-        patchNum: String,
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        draft: {
-          type: String,
-          value: '',
-        },
-        labels: Object,
-        permittedLabels: Object,
-
-        _account: Object,
-        _drafts: Object,
-        _xhrPromise: Object,  // Used for testing.
-      },
-
-      behaviors: [
-        Gerrit.RESTClientBehavior,
-      ],
-
-      ready: function() {
-        app.accountReady.then(function() {
-          this._account = app.account;
-        }.bind(this));
-      },
-
-      reload: function() {
-        return this.$.draftsXHR.generateRequest().completes;
-      },
-
-      focus: function() {
-        this.async(function() {
-          this.$.textarea.textarea.focus();
-        }.bind(this));
-      },
-
-      _computeDraftsURL: function(changeNum) {
-        return '/changes/' + changeNum + '/drafts';
-      },
-
-      _computeHideDraftList: function(drafts) {
-        return Object.keys(drafts || {}).length == 0;
-      },
-
-      _computeDraftsTitle: function(drafts) {
-        var total = 0;
-        for (var file in drafts) {
-          total += drafts[file].length;
-        }
-        if (total == 0) { return ''; }
-        if (total == 1) { return '1 Draft'; }
-        if (total > 1) { return total + ' Drafts'; }
-      },
-
-      _computeLabelArray: function(labelsObj) {
-        return Object.keys(labelsObj).sort();
-      },
-
-      _computeIndexOfLabelValue: function(
-          labels, permittedLabels, labelName, account) {
-        var t = labels[labelName];
-        if (!t) { return null; }
-        var labelValue = t.default_value;
-
-        // Is there an existing vote for the current user? If so, use that.
-        var votes = labels[labelName];
-        if (votes.all && votes.all.length > 0) {
-          for (var i = 0; i < votes.all.length; i++) {
-            if (votes.all[i]._account_id == account._account_id) {
-              labelValue = votes.all[i].value;
-              break;
-            }
-          }
-        }
-
-        var len = permittedLabels[labelName] != null ?
-            permittedLabels[labelName].length : 0;
-        for (var i = 0; i < len; i++) {
-          var val = parseInt(permittedLabels[labelName][i], 10);
-          if (val == labelValue) {
-            return i;
-          }
-        }
-        return null;
-      },
-
-      _computePermittedLabelValues: function(permittedLabels, label) {
-        return permittedLabels[label];
-      },
-
-      _cancelTapHandler: function(e) {
-        e.preventDefault();
-        this._drafts = null;
-        this.fire('cancel', null, {bubbles: false});
-      },
-
-      _sendTapHandler: function(e) {
-        e.preventDefault();
-        var obj = {
-          drafts: 'PUBLISH_ALL_REVISIONS',
-          labels: {},
-        };
-        for (var label in this.permittedLabels) {
-          var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
-          var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
-          selectedVal = parseInt(selectedVal, 10);
-          obj.labels[label] = selectedVal;
-        }
-        if (this.draft != null) {
-          obj.message = this.draft;
-        }
-        this.disabled = true;
-        this._send(obj).then(function(req) {
-          this.fire('send', null, {bubbles: false});
-          this.draft = '';
-          this.disabled = false;
-          this._drafts = null;
-        }.bind(this)).catch(function(err) {
-          alert('Oops. Something went wrong. Check the console and bug the ' +
-              'PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
-      },
-
-      _send: function(payload) {
-        var xhr = document.createElement('gr-request');
-        this._xhrPromise = xhr.send({
-          method: 'POST',
-          url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
-          body: payload,
-        });
-
-        return this._xhrPromise;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-request.html b/polygerrit-ui/app/elements/gr-request.html
deleted file mode 100644
index d6d3a95..0000000
--- a/polygerrit-ui/app/elements/gr-request.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-ajax/iron-request.html">
-
-<dom-module id="gr-request">
-  <template>
-    <iron-request id="xhr"></iron-request>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-request',
-
-      hostAttributes: {
-        hidden: true
-      },
-
-      send: function(options) {
-        options.headers = options.headers || {};
-        if (options.body != null) {
-          options.headers['content-type'] =
-              options.headers['content-type'] || 'application/json';
-        }
-        options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] ||
-            util.getCookie('XSRF_TOKEN');
-        options.jsonPrefix = options.jsonPrefix || ')]}\'';
-        return this.$.xhr.send(options);
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reviewer-list.html b/polygerrit-ui/app/elements/gr-reviewer-list.html
deleted file mode 100644
index 2b7f0c5..0000000
--- a/polygerrit-ui/app/elements/gr-reviewer-list.html
+++ /dev/null
@@ -1,450 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-button.html">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-reviewer-list">
-  <style>
-    :host {
-      display: block;
-    }
-    :host([disabled]) {
-      opacity: .8;
-      pointer-events: none;
-    }
-    .autocompleteContainer {
-      position: relative;
-    }
-    .inputContainer {
-      display: flex;
-      margin-top: .25em;
-    }
-    .inputContainer input {
-      flex: 1;
-      font: inherit;
-    }
-    .dropdown {
-      background-color: #fff;
-      box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-      position: absolute;
-      left: 0;
-      top: 100%;
-    }
-    .dropdown .reviewer {
-      cursor: pointer;
-      padding: .5em .75em;
-    }
-    .dropdown .reviewer[selected] {
-      background-color: #ccc;
-    }
-    .remove,
-    .cancel {
-      color: #999;
-    }
-    .remove {
-      font-size: .9em;
-    }
-    .cancel {
-      font-size: 2em;
-      line-height: 1;
-      padding: 0 .15em;
-      text-decoration: none;
-    }
-  </style>
-  <template>
-    <gr-ajax id="autocompleteXHR"
-        url="[[_computeAutocompleteURL(change)]]"
-        params="[[_computeAutocompleteParams(_inputVal)]]"
-        on-response="_handleResponse"></gr-ajax>
-
-    <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
-      <div class="reviewer">
-        <gr-account-link account="[[reviewer]]" show-email></gr-account-link>
-        <gr-button link
-            class="remove"
-            data-account-id$="[[reviewer._account_id]]"
-            on-tap="_handleRemoveTap"
-            hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
-      </div>
-    </template>
-    <div class="controlsContainer" hidden$="[[!mutable]]">
-      <div class="autocompleteContainer" hidden$="[[!_showInput]]">
-        <div class="inputContainer">
-          <input is="iron-input" id="input"
-              bind-value="{{_inputVal}}" disabled$="[[disabled]]">
-          <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
-        </div>
-        <div class="dropdown" hidden$="[[_hideAutocomplete]]">
-          <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
-            <div class="reviewer"
-                data-index$="[[index]]"
-                on-mouseenter="_handleMouseEnterItem"
-                on-tap="_handleItemTap"
-                selected$="[[_computeSelected(index, _selectedIndex)]]">
-              <template is="dom-if" if="[[reviewer.account]]">
-                <gr-account-label
-                    account="[[reviewer.account]]" show-email></gr-account-label>
-              </template>
-              <template is="dom-if" if="[[reviewer.group]]">
-                <span>[[reviewer.group.name]] (group)</span>
-              </template>
-            </div>
-          </template>
-        </div>
-      </div>
-      <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
-          hidden$="[[_showInput]]">Add reviewer</gr-button>
-    </div>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-reviewer-list',
-
-      properties: {
-        change: Object,
-        mutable: {
-          type: Boolean,
-          value: false,
-        },
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        suggestFrom: {
-          type: Number,
-          value: 3,
-        },
-
-        _reviewers: {
-          type: Array,
-          value: function() { return []; },
-        },
-        _autocompleteData: {
-          type: Array,
-          value: function() { return []; },
-          observer: '_autocompleteDataChanged',
-        },
-        _inputVal: {
-          type: String,
-          value: '',
-          observer: '_inputValChanged',
-        },
-        _inputRequestHandle: Number,
-        _inputRequestTimeout: {
-          type: Number,
-          value: 250,
-        },
-        _showInput: {
-          type: Boolean,
-          value: false,
-        },
-        _hideAutocomplete: {
-          type: Boolean,
-          value: true,
-          observer: '_hideAutocompleteChanged',
-        },
-        _selectedIndex: {
-          type: Number,
-          value: 0,
-        },
-        _boundBodyClickHandler: {
-          type: Function,
-          value: function() {
-            return this._handleBodyClick.bind(this);
-          },
-        },
-
-        // Used for testing.
-        _lastAutocompleteRequest: Object,
-        _xhrPromise: Object,
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-      ],
-
-      observers: [
-        '_reviewersChanged(change.reviewers.*, change.owner)',
-      ],
-
-      detached: function() {
-        this._clearInputRequestHandle();
-      },
-
-      _clearInputRequestHandle: function() {
-        if (this._inputRequestHandle != null) {
-          this.cancelAsync(this._inputRequestHandle);
-          this._inputRequestHandle = null;
-        }
-      },
-
-      _reviewersChanged: function(changeRecord, owner) {
-        var result = [];
-        var reviewers = changeRecord.base;
-        for (var key in reviewers) {
-          if (key == 'REVIEWER' || key == 'CC') {
-            result = result.concat(reviewers[key]);
-          }
-        }
-        this._reviewers = result.filter(function(reviewer) {
-          return reviewer._account_id != owner._account_id;
-        });
-      },
-
-      _computeCanRemoveReviewer: function(reviewer, mutable) {
-        if (!mutable) { return false; }
-
-        for (var i = 0; i < this.change.removable_reviewers.length; i++) {
-          if (this.change.removable_reviewers[i]._account_id ==
-              reviewer._account_id) {
-            return true;
-          }
-        }
-        return false;
-      },
-
-      _computeAutocompleteURL: function(change) {
-        return '/changes/' + change._number + '/suggest_reviewers';
-      },
-
-      _computeAutocompleteParams: function(inputVal) {
-        return {
-          n: 10,  // Return max 10 results
-          q: inputVal,
-        };
-      },
-
-      _computeSelected: function(index, selectedIndex) {
-        return index == selectedIndex;
-      },
-
-      _handleResponse: function(e) {
-        this._autocompleteData = e.detail.response.filter(function(reviewer) {
-          var account = reviewer.account;
-          if (!account) { return true; }
-          for (var i = 0; i < this._reviewers.length; i++) {
-            if (account._account_id == this.change.owner._account_id ||
-                account._account_id == this._reviewers[i]._account_id) {
-              return false;
-            }
-          }
-          return true;
-        }, this);
-      },
-
-      _handleBodyClick: function(e) {
-        var eventPath = Polymer.dom(e).path;
-        for (var i = 0; i < eventPath.length; i++) {
-          if (eventPath[i] == this) {
-            return;
-          }
-        }
-        this._selectedIndex = -1;
-        this._autocompleteData = [];
-      },
-
-      _handleRemoveTap: function(e) {
-        e.preventDefault();
-        var target = Polymer.dom(e).rootTarget;
-        var accountID = parseInt(target.getAttribute('data-account-id'), 10);
-        this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
-          var reviewers = this.change.reviewers;
-          ['REVIEWER', 'CC'].forEach(function(type) {
-            reviewers[type] = reviewers[type] || [];
-            for (var i = 0; i < reviewers[type].length; i++) {
-              if (reviewers[type][i]._account_id == accountID) {
-                this.splice('change.reviewers.' + type, i, 1);
-                break;
-              }
-            }
-          }, this);
-        }.bind(this)).catch(function(err) {
-          alert('Oops. Something went wrong. Check the console and bug the ' +
-              'PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
-      },
-
-      _handleAddTap: function(e) {
-        e.preventDefault();
-        this._showInput = true;
-        this.$.input.focus();
-      },
-
-      _handleCancelTap: function(e) {
-        e.preventDefault();
-        this._cancel();
-      },
-
-      _handleMouseEnterItem: function(e) {
-        this._selectedIndex =
-            parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10);
-      },
-
-      _handleItemTap: function(e) {
-        var reviewerEl;
-        var eventPath = Polymer.dom(e).path;
-        for (var i = 0; i < eventPath.length; i++) {
-          var el = eventPath[i];
-          if (el.classList && el.classList.contains('reviewer')) {
-            reviewerEl = el;
-            break;
-          }
-        }
-        this._selectedIndex =
-            parseInt(reviewerEl.getAttribute('data-index'), 10);
-        this._sendAddRequest();
-      },
-
-      _autocompleteDataChanged: function(data) {
-        this._hideAutocomplete = data.length == 0;
-      },
-
-      _hideAutocompleteChanged: function(hidden) {
-        if (hidden) {
-          document.body.removeEventListener('click',
-              this._boundBodyClickHandler);
-          this._selectedIndex = -1;
-        } else {
-          document.body.addEventListener('click', this._boundBodyClickHandler);
-          this._selectedIndex = 0;
-        }
-      },
-
-      _inputValChanged: function(val) {
-        var sendRequest = function() {
-          if (this.disabled || val == null || val.trim().length == 0) {
-            return;
-          }
-          if (val.length < this.suggestFrom) {
-            this._clearInputRequestHandle();
-            this._hideAutocomplete = true;
-            this._selectedIndex = -1;
-            return;
-          }
-          this._lastAutocompleteRequest =
-              this.$.autocompleteXHR.generateRequest();
-        }.bind(this);
-
-        this._clearInputRequestHandle();
-        if (this._inputRequestTimeout == 0) {
-          sendRequest();
-        } else {
-          this._inputRequestHandle =
-              this.async(sendRequest, this._inputRequestTimeout);
-        }
-      },
-
-      _handleKey: function(e) {
-        if (this._hideAutocomplete) {
-          if (e.keyCode == 27) {  // 'esc'
-            e.preventDefault();
-            this._cancel();
-          }
-          return;
-        }
-
-        switch (e.keyCode) {
-          case 38:  // 'up':
-            e.preventDefault();
-            this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
-            break;
-          case 40:  // 'down'
-            e.preventDefault();
-            this._selectedIndex = Math.min(this._selectedIndex + 1,
-                                           this._autocompleteData.length - 1);
-            break;
-          case 27:  // 'esc'
-            e.preventDefault();
-            this._hideAutocomplete = true;
-            break;
-          case 13:  // 'enter'
-            e.preventDefault();
-            this._sendAddRequest();
-            break;
-        }
-      },
-
-      _cancel: function() {
-        this._showInput = false;
-        this._selectedIndex = 0;
-        this._inputVal = '';
-        this._autocompleteData = [];
-        this.$.addReviewer.focus();
-      },
-
-      _sendAddRequest: function() {
-        this._clearInputRequestHandle();
-
-        var reviewerID;
-        var reviewer = this._autocompleteData[this._selectedIndex];
-        if (reviewer.account) {
-          reviewerID = reviewer.account._account_id;
-        } else if (reviewer.group) {
-          reviewerID = reviewer.group.id;
-        }
-        this._autocompleteData = [];
-        this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
-          this.change.reviewers.CC = this.change.reviewers.CC || [];
-          req.response.reviewers.forEach(function(r) {
-            this.push('change.removable_reviewers', r);
-            this.push('change.reviewers.CC', r);
-          }, this);
-          this._inputVal = '';
-          this.$.input.focus();
-        }.bind(this)).catch(function(err) {
-          // TODO(andybons): Use the message returned by the server.
-          alert('Unable to add ' + reviewerID + ' as a reviewer.');
-          throw err;
-        }.bind(this));
-      },
-
-      _send: function(method, url, reviewerID) {
-        this.disabled = true;
-        var request = document.createElement('gr-request');
-        var opts = {
-          method: method,
-          url: url,
-        };
-        if (reviewerID) {
-          opts.body = {reviewer: reviewerID};
-        }
-        this._xhrPromise = request.send(opts);
-        var enableEl = function() { this.disabled = false; }.bind(this);
-        this._xhrPromise.then(enableEl).catch(enableEl);
-        return this._xhrPromise;
-      },
-
-      _restEndpoint: function(id) {
-        var path = '/changes/' + this.change._number + '/reviewers';
-        if (id) {
-          path += '/' + id;
-        }
-        return path;
-      },
-    });
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html
deleted file mode 100644
index 293474c..0000000
--- a/polygerrit-ui/app/elements/gr-search-bar.html
+++ /dev/null
@@ -1,115 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="gr-button.html">
-
-<dom-module id="gr-search-bar">
-  <template>
-    <style>
-      :host {
-        display: inline-block;
-      }
-      form {
-        display: flex;
-      }
-      input {
-        border: 1px solid #d1d2d3;
-        outline: none;
-      }
-      input {
-        flex: 1;
-        font: inherit;
-        border-radius: 2px 0 0 2px;
-      }
-      gr-button {
-        background-color: #f1f2f3;
-        border-radius: 0 2px 2px 0;
-        border-left-width: 0;
-      }
-    </style>
-    <form>
-      <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
-      <gr-button id="searchButton">Search</gr-button>
-    </form>
-  </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-search-bar',
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-      ],
-
-      listeners: {
-        'searchInput.keydown': '_inputKeyDownHandler',
-        'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
-      },
-
-      properties: {
-        value: {
-          type: String,
-          value: '',
-          notify: true,
-          observer: '_valueChanged',
-        },
-        keyEventTarget: {
-          type: Object,
-          value: function() { return document.body; },
-        },
-
-        _inputVal: String,
-      },
-
-      _valueChanged: function(value) {
-        this._inputVal = value;
-      },
-
-      _inputKeyDownHandler: function(e) {
-        if (e.keyCode == 13) {  // Enter key
-          this._preventDefaultAndNavigateToInputVal(e);
-        }
-      },
-
-      _preventDefaultAndNavigateToInputVal: function(e) {
-        e.preventDefault();
-        Polymer.dom(e).rootTarget.blur();
-        page.show('/q/' + this._inputVal);
-      },
-
-      _handleKey: function(e) {
-        if (this.shouldSupressKeyboardShortcut(e)) { return; }
-        switch (e.keyCode) {
-          case 191:  // '/' or '?' with shift key.
-            // TODO(andybons): Localization using e.key/keypress event.
-            if (e.shiftKey) { break; }
-            e.preventDefault();
-            var s = this.$.searchInput;
-            s.focus();
-            s.setSelectionRange(0, s.value.length);
-            break;
-        }
-      },
-    });
-
-  })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
similarity index 81%
rename from polygerrit-ui/app/elements/gr-account-dropdown.html
rename to polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
index 99850f6..7615d15 100644
--- a/polygerrit-ui/app/elements/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
@@ -14,9 +14,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="gr-button.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../gr-button/gr-button.html">
 
 <dom-module id="gr-account-dropdown">
   <style>
@@ -78,21 +78,5 @@
       </div>
     </iron-dropdown>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-account-dropdown',
-
-      properties: {
-        account: Object,
-      },
-
-      _showDropdownTapHandler: function(e) {
-        this.$.dropdown.open();
-      },
-    });
-  })();
-  </script>
+  <script src="gr-account-dropdown.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
new file mode 100644
index 0000000..09de6c1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
@@ -0,0 +1,28 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-account-dropdown',
+
+    properties: {
+      account: Object,
+    },
+
+    _showDropdownTapHandler: function(e) {
+      this.$.dropdown.open();
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-account-dropdown-test.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
similarity index 78%
rename from polygerrit-ui/app/test/gr-account-dropdown-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
index 795d569..3ae3b14 100644
--- a/polygerrit-ui/app/test/gr-account-dropdown-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-dropdown</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-account-dropdown.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-dropdown.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
similarity index 60%
rename from polygerrit-ui/app/elements/gr-account-label.html
rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index 7d35698..b7f9715 100644
--- a/polygerrit-ui/app/elements/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-avatar.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-avatar/gr-avatar.html">
 
 <dom-module id="gr-account-label">
   <template>
@@ -44,39 +44,5 @@
       </span>
     </span>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-account-label',
-
-      properties: {
-        account: Object,
-        avatarImageSize: {
-          type: Number,
-          value: 32,
-        },
-        showEmail: {
-          type: Boolean,
-          value: false,
-        },
-      },
-
-      _computeAccountTitle: function(account) {
-        if (!account || !account.name) { return; }
-        var result = util.escapeHTML(account.name);
-        if (account.email) {
-          result += ' <' + util.escapeHTML(account.email) + '>';
-        }
-        return result;
-      },
-
-      _computeShowEmail: function(showEmail, account) {
-        return !!(showEmail && account && account.email);
-      },
-
-    });
-  })();
-  </script>
+  <script src="gr-account-label.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
new file mode 100644
index 0000000..98871cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -0,0 +1,45 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-account-label',
+
+    properties: {
+      account: Object,
+      avatarImageSize: {
+        type: Number,
+        value: 32,
+      },
+      showEmail: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    _computeAccountTitle: function(account) {
+      if (!account || !account.name) { return; }
+      var result = util.escapeHTML(account.name);
+      if (account.email) {
+        result += ' <' + util.escapeHTML(account.email) + '>';
+      }
+      return result;
+    },
+
+    _computeShowEmail: function(showEmail, account) {
+      return !!(showEmail && account && account.email);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-account-label-test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
similarity index 85%
rename from polygerrit-ui/app/test/gr-account-label-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index dbfdeec..c39f288 100644
--- a/polygerrit-ui/app/test/gr-account-label-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-label</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../elements/gr-account-label.html">
+<link rel="import" href="gr-account-label.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
similarity index 65%
rename from polygerrit-ui/app/elements/gr-account-link.html
rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 8b0b726..d3585ef 100644
--- a/polygerrit-ui/app/elements/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-account-label.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-account-label/gr-account-label.html">
 
 <dom-module id="gr-account-link">
   <template>
@@ -40,28 +40,5 @@
       </a>
     </span>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-account-link',
-
-      properties: {
-        account: Object,
-        avatarImageSize: {
-          type: Number,
-          value: 32,
-        },
-      },
-
-      _computeOwnerLink: function(account) {
-        if (!account) { return; }
-        var accountID = account.email || account._account_id;
-        return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
-      },
-
-    });
-  })();
-  </script>
+  <script src="gr-account-link.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
new file mode 100644
index 0000000..058b27d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -0,0 +1,34 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-account-link',
+
+    properties: {
+      account: Object,
+      avatarImageSize: {
+        type: Number,
+        value: 32,
+      },
+    },
+
+    _computeOwnerLink: function(account) {
+      if (!account) { return; }
+      var accountID = account.email || account._account_id;
+      return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-account-link-test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
similarity index 80%
rename from polygerrit-ui/app/test/gr-account-link-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index dfc27f8..e1ef862 100644
--- a/polygerrit-ui/app/test/gr-account-link-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -18,12 +18,12 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-link</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../elements/gr-account-link.html">
+<link rel="import" href="gr-account-link.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
new file mode 100644
index 0000000..9a93426
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
@@ -0,0 +1,35 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-ajax/iron-ajax.html">
+
+<dom-module id="gr-ajax">
+  <template>
+    <iron-ajax id="xhr"
+        auto="[[auto]]"
+        url="[[url]]"
+        params="[[params]]"
+        json-prefix=")]}'"
+        last-error="{{lastError}}"
+        last-response="{{lastResponse}}"
+        loading="{{loading}}"
+        on-response="_handleResponse"
+        on-error="_handleError"
+        debounce-duration="300"></iron-ajax>
+  </template>
+  <script src="gr-ajax.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
new file mode 100644
index 0000000..7fec507
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
@@ -0,0 +1,81 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-ajax',
+
+    /**
+     * Fired when a response is received.
+     *
+     * @event response
+     */
+
+    /**
+     * Fired when an error is received.
+     *
+     * @event error
+     */
+
+    hostAttributes: {
+      hidden: true
+    },
+
+    properties: {
+      auto: {
+        type: Boolean,
+        value: false,
+      },
+      url: String,
+      params: {
+        type: Object,
+        value: function() {
+          return {};
+        },
+      },
+      lastError: {
+        type: Object,
+        notify: true,
+      },
+      lastResponse: {
+        type: Object,
+        notify: true,
+      },
+      loading: {
+        type: Boolean,
+        notify: true,
+      },
+    },
+
+    ready: function() {
+      // Used for debugging which element a request came from.
+      var headers = this.$.xhr.headers;
+      headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)';
+      this.$.xhr.headers = headers;
+    },
+
+    generateRequest: function() {
+      return this.$.xhr.generateRequest();
+    },
+
+    _handleResponse: function(e, req) {
+      this.fire('response', req, {bubbles: false});
+    },
+
+    _handleError: function(e, req) {
+      this.fire('error', req, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
new file mode 100644
index 0000000..3491443
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -0,0 +1,31 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-avatar">
+  <template>
+    <style>
+      :host {
+        display: inline-block;
+        border-radius: 50%;
+        background-size: cover;
+        background-color: var(--background-color, #f1f2f3);
+      }
+    </style>
+  </template>
+  <script src="gr-avatar.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
new file mode 100644
index 0000000..8f289ca
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -0,0 +1,63 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-avatar',
+
+    properties: {
+      account: {
+        type: Object,
+        observer: '_accountChanged',
+      },
+      imageSize: {
+        type: Number,
+        value: 16,
+      },
+    },
+
+    created: function() {
+      this.hidden = true;
+    },
+
+    ready: function() {
+      app.configReady.then(function(cfg) {
+        var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+        if (hasAvatars) {
+          this.hidden = false;
+          this._updateAvatarURL(this.account); // src needs to be set if avatar becomes visible
+        }
+      }.bind(this));
+    },
+
+    _accountChanged: function(account) {
+      this._updateAvatarURL(account);
+    },
+
+    _updateAvatarURL: function(account) {
+      if (!this.hidden && account) {
+        var url = this._buildAvatarURL(this.account);
+        if (url) {
+          this.style.backgroundImage = 'url("' + url + '")';
+        }
+      }
+    },
+
+    _buildAvatarURL: function(account) {
+      if (!account) { return ''; }
+      return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-avatar-test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
similarity index 86%
rename from polygerrit-ui/app/test/gr-avatar-test.html
rename to polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index ed80c3b..7e3c25c 100644
--- a/polygerrit-ui/app/test/gr-avatar-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-avatar</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
 
-<link rel="import" href="../elements/gr-avatar.html">
+<link rel="import" href="gr-avatar.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
similarity index 70%
rename from polygerrit-ui/app/elements/gr-button.html
rename to polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 33b7871..1df98fd 100644
--- a/polygerrit-ui/app/elements/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 
 <dom-module id="gr-button">
   <template strip-whitespace>
@@ -102,50 +102,5 @@
     </style>
     <content></content>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-button',
-
-      properties: {
-        disabled: {
-          type: Boolean,
-          observer: '_disabledChanged',
-          reflectToAttribute: true,
-        },
-        _enabledTabindex: {
-          type: String,
-          value: '0',
-        },
-      },
-
-      behaviors: [
-        Gerrit.KeyboardShortcutBehavior,
-      ],
-
-      hostAttributes: {
-        role: 'button',
-        tabindex: '0',
-      },
-
-      _disabledChanged: function(disabled) {
-        if (disabled) {
-          this._enabledTabindex = this.getAttribute('tabindex');
-        }
-        this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
-      },
-
-      _handleKey: function(e) {
-        switch (e.keyCode) {
-          case 32:  // 'spacebar'
-          case 13:  // 'enter'
-            e.preventDefault();
-            this.click();
-        }
-      },
-    });
-  })();
-  </script>
+  <script src="gr-button.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
new file mode 100644
index 0000000..772fccc
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -0,0 +1,57 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-button',
+
+    properties: {
+      disabled: {
+        type: Boolean,
+        observer: '_disabledChanged',
+        reflectToAttribute: true,
+      },
+      _enabledTabindex: {
+        type: String,
+        value: '0',
+      },
+    },
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    hostAttributes: {
+      role: 'button',
+      tabindex: '0',
+    },
+
+    _disabledChanged: function(disabled) {
+      if (disabled) {
+        this._enabledTabindex = this.getAttribute('tabindex');
+      }
+      this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
+    },
+
+    _handleKey: function(e) {
+      switch (e.keyCode) {
+        case 32:  // 'spacebar'
+        case 13:  // 'enter'
+          e.preventDefault();
+          this.click();
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
new file mode 100644
index 0000000..62a9d2d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -0,0 +1,51 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-request/gr-request.html">
+
+<dom-module id="gr-change-star">
+  <template>
+    <style>
+      :host {
+        display: inline-block;
+        overflow: hidden;
+      }
+      .starButton {
+        background-color: transparent;
+        border-color: transparent;
+        cursor: pointer;
+        font-size: 1.1em;
+        width: 1.2em;
+        height: 1.2em;
+        outline: none;
+      }
+      .starButton svg {
+        fill: #ccc;
+        width: 1em;
+        height: 1em;
+      }
+      .starButton-active svg {
+        fill: #ffac33;
+      }
+    </style>
+    <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap">
+      <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 -->
+      <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657  l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657  L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg>
+    </button>
+  </template>
+  <script src="gr-change-star.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
new file mode 100644
index 0000000..26680b6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -0,0 +1,61 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-change-star',
+
+    properties: {
+      change: {
+        type: Object,
+        notify: true,
+      },
+
+      _xhrPromise: Object,  // Used for testing.
+    },
+
+    _computeStarClass: function(starred) {
+      var classes = ['starButton'];
+      if (starred) {
+        classes.push('starButton-active');
+      }
+      return classes.join(' ');
+    },
+
+    _handleStarTap: function() {
+      var method = this.change.starred ? 'DELETE' : 'PUT';
+      this.set('change.starred', !this.change.starred);
+      this._send(method, this._restEndpoint()).catch(function(err) {
+        this.set('change.starred', !this.change.starred);
+        alert('Change couldn’t be starred. Check the console and contact ' +
+            'the PolyGerrit team for assistance.');
+        throw err;
+      }.bind(this));
+    },
+
+    _send: function(method, url) {
+      var xhr = document.createElement('gr-request');
+      this._xhrPromise = xhr.send({
+        method: method,
+        url: url,
+      });
+      return this._xhrPromise;
+    },
+
+    _restEndpoint: function() {
+      return '/accounts/self/starred.changes/' + this.change._number;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-change-star-test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
similarity index 85%
rename from polygerrit-ui/app/test/gr-change-star-test.html
rename to polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 3f7632b..86ee947 100644
--- a/polygerrit-ui/app/test/gr-change-star-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -18,14 +18,14 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-star</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-star.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-star.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
new file mode 100644
index 0000000..d8fc1df
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -0,0 +1,48 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-button/gr-button.html">
+
+<dom-module id="gr-confirm-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      header {
+        border-bottom: 1px solid #ddd;
+        font-weight: bold;
+      }
+      header,
+      main,
+      footer {
+        padding: .5em .65em;
+      }
+      footer {
+        display: flex;
+        justify-content: space-between;
+      }
+    </style>
+    <header><content select=".header"></content></header>
+    <main><content select=".main"></content></main>
+    <footer>
+      <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button>
+      <gr-button on-tap="_handleCancelTap">Cancel</gr-button>
+    </footer>
+  </template>
+  <script src="gr-confirm-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
new file mode 100644
index 0000000..0f20e0a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      confirmLabel: {
+        type: String,
+        value: 'Confirm',
+      }
+    },
+
+    hostAttributes: {
+      role: 'dialog',
+    },
+
+    _handleConfirmTap: function(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-confirm-dialog-test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
similarity index 80%
rename from polygerrit-ui/app/test/gr-confirm-dialog-test.html
rename to polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
index d19118e..812f32a 100644
--- a/polygerrit-ui/app/test/gr-confirm-dialog-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-dialog</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-confirm-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-dialog.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
similarity index 60%
copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
copy to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 6abf8c2..6d4b2ea 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2016 The Android Open Source Project
+Copyright (C) 2015 The Android Open Source Project
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,11 +14,16 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
 
-<dom-module id="gr-rest-api-interface">
-  <template></template>
-  <script src="gr-rest-api-interface.js"></script>
+<dom-module id="gr-date-formatter">
+  <template>
+    <style>
+      :host {
+        display: inline;
+      }
+    </style>
+    <span>[[_computeDateStr(dateStr)]]</span>
+  </template>
+  <script src="gr-date-formatter.js"></script>
 </dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
new file mode 100644
index 0000000..cb77cc1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -0,0 +1,76 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var Duration = {
+    HOUR: 1000 * 60 * 60,
+    DAY: 1000 * 60 * 60 * 24,
+  };
+
+  var ShortMonthNames = [
+    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
+    'Nov', 'Dec'
+  ];
+
+  Polymer({
+    is: 'gr-date-formatter',
+
+    properties: {
+      dateStr: {
+        type: String,
+        value: null,
+        notify: true
+      }
+    },
+
+    _computeDateStr: function(dateStr) {
+      return this._dateStr(this._parseDateStr(dateStr), new Date());
+    },
+
+    _parseDateStr: function(dateStr) {
+      if (!dateStr) { return null; }
+      return util.parseDate(dateStr);
+    },
+
+    _dateStr: function(t, now) {
+      if (!t) { return ''; }
+      var diff = now.getTime() - t.getTime();
+      if (diff < Duration.DAY && t.getDay() == now.getDay()) {
+        // Within 24 hours and on the same day:
+        // '2:14 AM'
+        var pm = t.getHours() >= 12;
+        var hours = t.getHours();
+        if (hours == 0) {
+          hours = 12;
+        } else if (hours > 12) {
+          hours = t.getHours() - 12;
+        }
+        var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
+            t.getMinutes();
+        return hours + ':' + minutes + (pm ? ' PM' : ' AM');
+      } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
+                 diff < 180 * Duration.DAY) {
+        // From one to six months:
+        // 'Aug 29'
+        return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
+      } else if (diff >= 180 * Duration.DAY) {
+        // More than six months:
+        // 'Aug 29, 1997'
+        return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
+            t.getFullYear();
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-date-formatter-test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
similarity index 89%
rename from polygerrit-ui/app/test/gr-date-formatter-test.html
rename to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 10e8ce8..9bba517 100644
--- a/polygerrit-ui/app/test/gr-date-formatter-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-date-formatter</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../elements/gr-date-formatter.html">
+<link rel="import" href="gr-date-formatter.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
similarity index 86%
rename from polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html
rename to polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 7e80baa..9988c28 100644
--- a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-button.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-button/gr-button.html">
 
 <dom-module id="gr-keyboard-shortcuts-dialog">
   <template>
@@ -199,42 +199,5 @@
     </main>
     <footer></footer>
   </template>
-  <script>
-  (function() {
-    'use strict';
-
-    Polymer({
-      is: 'gr-keyboard-shortcuts-dialog',
-
-      /**
-       * Fired when the user presses the close button.
-       *
-       * @event close
-       */
-
-      properties: {
-        view: String,
-      },
-
-      hostAttributes: {
-        role: 'dialog',
-      },
-
-      _computeInView: function(currentView, view) {
-        return view == currentView;
-      },
-
-      _computeInChangeListView: function(currentView) {
-        return currentView == 'gr-change-list-view' ||
-            currentView == 'gr-dashboard-view';
-      },
-
-      _handleCloseTap: function(e) {
-        e.preventDefault();
-        this.fire('close', null, {bubbles: false});
-      },
-
-    });
-  })();
-  </script>
+  <script src="gr-keyboard-shortcuts-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
new file mode 100644
index 0000000..7ed5012
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-keyboard-shortcuts-dialog',
+
+    /**
+     * Fired when the user presses the close button.
+     *
+     * @event close
+     */
+
+    properties: {
+      view: String,
+    },
+
+    hostAttributes: {
+      role: 'dialog',
+    },
+
+    _computeInView: function(currentView, view) {
+      return view == currentView;
+    },
+
+    _computeInChangeListView: function(currentView) {
+      return currentView == 'gr-change-list-view' ||
+          currentView == 'gr-dashboard-view';
+    },
+
+    _handleCloseTap: function(e) {
+      e.preventDefault();
+      this.fire('close', null, {bubbles: false});
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/scripts/ba-linkify.js b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
similarity index 100%
rename from polygerrit-ui/app/scripts/ba-linkify.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
new file mode 100644
index 0000000..68a98e8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -0,0 +1,39 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<script src="ba-linkify.js"></script>
+<script src="link-text-parser.js"></script>
+<dom-module id="gr-linked-text">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      :host([pre]) span {
+        white-space: pre-wrap;
+        word-wrap: break-word;
+      }
+      :host([disabled]) a {
+        color: inherit;
+        text-decoration: none;
+        pointer-events: none;
+      }
+    </style>
+    <span id="output"></span>
+  </template>
+  <script src="gr-linked-text.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
new file mode 100644
index 0000000..cb852fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -0,0 +1,76 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-linked-text',
+
+    properties: {
+      content: {
+        type: String,
+        observer: '_contentChanged',
+      },
+      pre: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      config: Object,
+    },
+
+    observers: [
+      '_contentOrConfigChanged(content, config)',
+    ],
+
+    _contentChanged: function(content) {
+      // In the case where the config may not be set (perhaps due to the
+      // request for it still being in flight), set the content anyway to
+      // prevent waiting on the config to display the text.
+      if (this.config != null) { return; }
+      this.$.output.textContent = content;
+    },
+
+    _contentOrConfigChanged: function(content, config) {
+      var output = Polymer.dom(this.$.output);
+      output.textContent = '';
+      var parser = new GrLinkTextParser(config, function(text, href, html) {
+        if (href) {
+          var a = document.createElement('a');
+          a.href = href;
+          a.textContent = text;
+          a.target = '_blank';
+          output.appendChild(a);
+        } else if (html) {
+          var fragment = document.createDocumentFragment();
+          // Create temporary div to hold the nodes in.
+          var div = document.createElement('div');
+          div.innerHTML = html;
+          while (div.firstChild) {
+            fragment.appendChild(div.firstChild);
+          }
+          output.appendChild(fragment);
+        } else {
+          output.appendChild(document.createTextNode(text));
+        }
+      });
+      parser.parse(content);
+    }
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-linked-text-test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-linked-text-test.html
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 0761948..08ee24b 100644
--- a/polygerrit-ui/app/test/gr-linked-text-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -18,11 +18,11 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-linked-text</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
 
-<link rel="import" href="../elements/gr-linked-text.html">
+<link rel="import" href="gr-linked-text.html">
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/scripts/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
similarity index 100%
rename from polygerrit-ui/app/scripts/link-text-parser.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
new file mode 100644
index 0000000..817d8c5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -0,0 +1,32 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+
+<dom-module id="gr-overlay">
+  <template>
+    <style>
+      :host {
+        background: #fff;
+        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+      }
+    </style>
+    <content></content>
+  </template>
+  <script src="gr-overlay.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
new file mode 100644
index 0000000..5fa33ea
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -0,0 +1,44 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-overlay',
+
+    behaviors: [
+      Polymer.IronOverlayBehavior,
+    ],
+
+    detached: function() {
+      // For good measure.
+      Gerrit.KeyboardShortcutBehavior.enabled = true;
+    },
+
+    open: function() {
+      Gerrit.KeyboardShortcutBehavior.enabled = false;
+      Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments);
+    },
+
+    close: function() {
+      Gerrit.KeyboardShortcutBehavior.enabled = true;
+      Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments);
+    },
+
+    cancel: function() {
+      Gerrit.KeyboardShortcutBehavior.enabled = true;
+      Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
similarity index 60%
copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
copy to polygerrit-ui/app/elements/shared/gr-request/gr-request.html
index 6abf8c2..df9eddc 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2016 The Android Open Source Project
+Copyright (C) 2015 The Android Open Source Project
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,11 +14,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-ajax/iron-request.html">
 
-<dom-module id="gr-rest-api-interface">
-  <template></template>
-  <script src="gr-rest-api-interface.js"></script>
+<dom-module id="gr-request">
+  <template>
+    <iron-request id="xhr"></iron-request>
+  </template>
+  <script src="gr-request.js"></script>
 </dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.js b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
new file mode 100644
index 0000000..be24344
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
@@ -0,0 +1,36 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-request',
+
+    hostAttributes: {
+      hidden: true
+    },
+
+    send: function(options) {
+      options.headers = options.headers || {};
+      if (options.body != null) {
+        options.headers['content-type'] =
+            options.headers['content-type'] || 'application/json';
+      }
+      options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] ||
+          util.getCookie('XSRF_TOKEN');
+      options.jsonPrefix = options.jsonPrefix || ')]}\'';
+      return this.$.xhr.send(options);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
similarity index 83%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 6abf8c2..10c8a29 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<script src="../../../bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
   <template></template>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
similarity index 100%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
similarity index 88%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 5ea81b9..71d747e 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,10 +18,10 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-rest-api-interface</title>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-rest-api-interface.html">
 
 <test-fixture id="basic">
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
new file mode 100644
index 0000000..900245b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
@@ -0,0 +1,52 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../gr-button/gr-button.html">
+
+<dom-module id="gr-search-bar">
+  <template>
+    <style>
+      :host {
+        display: inline-block;
+      }
+      form {
+        display: flex;
+      }
+      input {
+        border: 1px solid #d1d2d3;
+        outline: none;
+      }
+      input {
+        flex: 1;
+        font: inherit;
+        border-radius: 2px 0 0 2px;
+      }
+      gr-button {
+        background-color: #f1f2f3;
+        border-radius: 0 2px 2px 0;
+        border-left-width: 0;
+      }
+    </style>
+    <form>
+      <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
+      <gr-button id="searchButton">Search</gr-button>
+    </form>
+  </template>
+  <script src="gr-search-bar.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
new file mode 100644
index 0000000..bef461d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
@@ -0,0 +1,74 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-search-bar',
+
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
+    listeners: {
+      'searchInput.keydown': '_inputKeyDownHandler',
+      'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
+    },
+
+    properties: {
+      value: {
+        type: String,
+        value: '',
+        notify: true,
+        observer: '_valueChanged',
+      },
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
+
+      _inputVal: String,
+    },
+
+    _valueChanged: function(value) {
+      this._inputVal = value;
+    },
+
+    _inputKeyDownHandler: function(e) {
+      if (e.keyCode == 13) {  // Enter key
+        this._preventDefaultAndNavigateToInputVal(e);
+      }
+    },
+
+    _preventDefaultAndNavigateToInputVal: function(e) {
+      e.preventDefault();
+      Polymer.dom(e).rootTarget.blur();
+      page.show('/q/' + this._inputVal);
+    },
+
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      switch (e.keyCode) {
+        case 191:  // '/' or '?' with shift key.
+          // TODO(andybons): Localization using e.key/keypress event.
+          if (e.shiftKey) { break; }
+          e.preventDefault();
+          var s = this.$.searchInput;
+          s.focus();
+          s.setSelectionRange(0, s.value.length);
+          break;
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/test/gr-search-bar-test.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
similarity index 82%
rename from polygerrit-ui/app/test/gr-search-bar-test.html
rename to polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
index c8a5ea7..84752e4 100644
--- a/polygerrit-ui/app/test/gr-search-bar-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
@@ -18,13 +18,13 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-search-bar</title>
 
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
 
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-search-bar.html">
-<script src="../scripts/util.js"></script>
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-search-bar.html">
+<script src="../../../scripts/util.js"></script>
 
 <test-fixture id="basic">
   <template>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 66230cb..fcfaaa3 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -24,36 +24,36 @@
   var testFiles = [];
 
   [
-    '../elements/gr-rest-api-interface/gr-rest-api-interface_test.html',
-    'gr-account-dropdown-test.html',
-    'gr-account-label-test.html',
-    'gr-account-link-test.html',
-    'gr-avatar-test.html',
-    'gr-change-actions-test.html',
-    'gr-change-list-item-test.html',
-    'gr-change-list-test.html',
-    'gr-change-metadata-test.html',
-    'gr-change-star-test.html',
-    'gr-change-view-test.html',
-    'gr-confirm-dialog-test.html',
-    'gr-confirm-rebase-dialog-test.html',
-    'gr-date-formatter-test.html',
-    'gr-diff-comment-test.html',
-    'gr-diff-comment-thread-test.html',
-    'gr-diff-preferences-test.html',
-    'gr-diff-side-test.html',
-    'gr-diff-test.html',
-    'gr-diff-view-test.html',
-    'gr-download-dialog-test.html',
-    'gr-file-list-test.html',
-    'gr-linked-text-test.html',
-    'gr-message-test.html',
-    'gr-messages-list-test.html',
-    'gr-patch-range-select-test.html',
-    'gr-related-changes-list-test.html',
-    'gr-reply-dialog-test.html',
-    'gr-reviewer-list-test.html',
-    'gr-search-bar-test.html',
+    '../elements/change/gr-change-actions/gr-change-actions_test.html',
+    '../elements/change/gr-change-metadata/gr-change-metadata_test.html',
+    '../elements/change/gr-change-view/gr-change-view_test.html',
+    '../elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
+    '../elements/change/gr-download-dialog/gr-download-dialog_test.html',
+    '../elements/change/gr-file-list/gr-file-list_test.html',
+    '../elements/change/gr-message/gr-message_test.html',
+    '../elements/change/gr-messages-list/gr-messages-list_test.html',
+    '../elements/change/gr-related-changes-list/gr-related-changes-list_test.html',
+    '../elements/change/gr-reply-dialog/gr-reply-dialog_test.html',
+    '../elements/change/gr-reviewer-list/gr-reviewer-list_test.html',
+    '../elements/change-list/gr-change-list/gr-change-list_test.html',
+    '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
+    '../elements/diff/gr-diff/gr-diff_test.html',
+    '../elements/diff/gr-diff-comment/gr-diff-comment_test.html',
+    '../elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
+    '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
+    '../elements/diff/gr-diff-side/gr-diff-side_test.html',
+    '../elements/diff/gr-diff-view/gr-diff-view_test.html',
+    '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
+    '../elements/shared/gr-account-dropdown/gr-account-dropdown_test.html',
+    '../elements/shared/gr-account-label/gr-account-label_test.html',
+    '../elements/shared/gr-account-link/gr-account-link_test.html',
+    '../elements/shared/gr-avatar/gr-avatar_test.html',
+    '../elements/shared/gr-change-star/gr-change-star_test.html',
+    '../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
+    '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
+    '../elements/shared/gr-linked-text/gr-linked-text_test.html',
+    '../elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
+    '../elements/shared/gr-search-bar/gr-search-bar_test.html',
   ].forEach(function(file) {
     testFiles.push(file);
     testFiles.push(file + '?dom=shadow');