Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/chdir/mr-activity-table/mr-activity-table.js b/static_src/elements/chdir/mr-activity-table/mr-activity-table.js
new file mode 100644
index 0000000..a0f4715
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-activity-table.js
@@ -0,0 +1,129 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+import './mr-day-icon.js';
+
+const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
+  'July', 'August', 'September', 'October', 'November', 'December'];
+const WEEKDAY_ABBREVIATIONS = 'M T W T F S S'.split(' ');
+const SECONDS_PER_DAY = 24 * 60 * 60;
+// Only show comments from this many days ago and later.
+const MAX_COMMENT_AGE = 31 * 3;
+
+export class MrActivityTable extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      :host {
+        display: grid;
+        grid-auto-flow: column;
+        grid-auto-columns: repeat(13, auto);
+        grid-template-rows: repeat(7, auto);
+        margin: auto;
+        width: 90%;
+        text-align: center;
+        line-height: 110%;
+        align-items: center;
+        justify-content: space-between;
+      }
+      :host[hidden] {
+        display: none;
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      ${WEEKDAY_ABBREVIATIONS.map((weekday) => html`<span>${weekday}</span>`)}
+      ${this._weekdayOffset.map(() => html`<span></span>`)}
+      ${this._activityArray.map((day) => html`
+        <mr-day-icon
+          .selected=${this.selectedDate === day.date}
+          .commentCount=${day.commentCount}
+          .date=${day.date}
+          @click=${this._selectDay}
+        ></mr-day-icon>
+      `)}
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      comments: {type: Array},
+      selectedDate: {type: Number},
+    };
+  }
+
+  _selectDay(event) {
+    const target = event.target;
+    if (this.selectedDate === target.date) {
+      this.selectedDate = undefined;
+    } else {
+      this.selectedDate = target.date;
+    }
+
+    this.dispatchEvent(new CustomEvent('dateChange', {
+      detail: {
+        date: this.selectedDate,
+      },
+    }));
+  }
+
+  get months() {
+    const currentMonth = (new Date()).getMonth();
+    return [MONTH_NAMES[currentMonth],
+      MONTH_NAMES[currentMonth - 1],
+      MONTH_NAMES[currentMonth - 2]];
+  }
+
+  get _weekdayOffset() {
+    const startDate = new Date(this._activityArray[0].date * 1000);
+    const startWeekdayNum = startDate.getDay()-1;
+    const emptyDays = [];
+    for (let i = 0; i < startWeekdayNum; i++) {
+      emptyDays.push(' ');
+    }
+    return emptyDays;
+  }
+
+  get _todayUnixTime() {
+    const now = new Date();
+    const today = new Date(Date.UTC(
+        now.getUTCFullYear(),
+        now.getUTCMonth(),
+        now.getUTCDate(),
+        24, 0, 0));
+    const todayEndTime = today.getTime() / 1000;
+    return todayEndTime;
+  }
+
+  get _activityArray() {
+    const todayUnixEndTime = this._todayUnixTime;
+    const comments = this.comments || [];
+
+    const activityArray = [];
+    for (let i = 0; i < MAX_COMMENT_AGE; i++) {
+      const arrayDate = (todayUnixEndTime - ((i) * SECONDS_PER_DAY));
+      activityArray.unshift({
+        commentCount: 0,
+        date: arrayDate,
+      });
+    }
+
+    for (let i = 0; i < comments.length; i++) {
+      const commentAge = Math.floor(
+          (todayUnixEndTime - comments[i].timestamp) / SECONDS_PER_DAY);
+      if (commentAge < MAX_COMMENT_AGE) {
+        const pos = MAX_COMMENT_AGE - commentAge - 1;
+        activityArray[pos].commentCount++;
+      }
+    }
+
+    return activityArray;
+  }
+}
+customElements.define('mr-activity-table', MrActivityTable);
diff --git a/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js b/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js
new file mode 100644
index 0000000..0eb9d30
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js
@@ -0,0 +1,57 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrActivityTable} from './mr-activity-table.js';
+import sinon from 'sinon';
+
+const SECONDS_PER_DAY = 24 * 60 * 60;
+
+let element;
+
+describe('mr-activity-table', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-activity-table');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrActivityTable);
+  });
+
+  it('no comments makes empty activity array', () => {
+    element.comments = [];
+
+    for (let i = 0; i < 93; i++) {
+      assert.equal(0, element._activityArray[i].commentCount);
+    }
+  });
+
+  it('activity array handles old comments', () => {
+    // 94 days since EPOCH.
+    sinon.stub(element, '_todayUnixTime').get(() => 94 * SECONDS_PER_DAY);
+
+    element.comments = [
+      {content: 'blah', timestamp: 0}, // too old.
+      {content: 'ignore', timestamp: 100}, // too old.
+      {
+        content: 'comment',
+        timestamp: SECONDS_PER_DAY + 1, // barely young enough.
+      },
+      {content: 'hello', timestamp: SECONDS_PER_DAY + 10}, // same day as above.
+      {content: 'world', timestamp: SECONDS_PER_DAY * 94}, // today
+    ];
+
+    assert.equal(93, element._activityArray.length);
+    assert.equal(2, element._activityArray[0].commentCount);
+    for (let i = 1; i < 92; i++) {
+      assert.equal(0, element._activityArray[i].commentCount);
+    }
+    assert.equal(1, element._activityArray[92].commentCount);
+  });
+});
diff --git a/static_src/elements/chdir/mr-activity-table/mr-day-icon.js b/static_src/elements/chdir/mr-activity-table/mr-day-icon.js
new file mode 100644
index 0000000..82f62b3
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-day-icon.js
@@ -0,0 +1,91 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+export class MrDayIcon extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      :host {
+        background-color: hsl(0, 0%, 95%);
+        margin: 0.25em 8px;
+        height: 20px;
+        width: 20px;
+        border: 2px solid white;
+        transition: border-color .5s ease-in-out;
+      }
+      :host(:hover) {
+        cursor: pointer;
+        border-color: hsl(87, 20%, 45%);
+      }
+      :host([activityLevel="0"]) {
+        background-color: var(--chops-blue-gray-50);
+      }
+      :host([activityLevel="1"]) {
+        background-color: hsl(87, 70%, 87%);
+      }
+      :host([activityLevel="2"]) {
+        background-color: hsl(88, 67%, 72%);
+      }
+      :host([activityLevel="3"]) {
+        background-color: hsl(87, 80%, 40%);
+      }
+      :host([selected]) {
+        border-color: hsl(0, 0%, 13%);
+      }
+      .hover-card {
+        display: none;
+      }
+      :host(:hover) .hover-card {
+        display: block;
+        position: relative;
+        width: 150px;
+        padding: 0.5em 8px;
+        background: rgba(0, 0, 0, 0.6);
+        color: var(--chops-white);
+        border-radius: 8px;
+        top: 120%;
+        left: 50%;
+        transform: translateX(-50%);
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <div class="hover-card">
+        ${this.commentCount} Comments<br>
+        <chops-timestamp .timestamp=${this.date}></chops-timestamp>
+      </div>
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      activityLevel: {
+        type: Number,
+        reflect: true,
+      },
+      commentCount: {type: Number},
+      date: {type: Number},
+      selected: {
+        type: Boolean,
+        reflect: true,
+      },
+    };
+  }
+
+  /** @override */
+  update(changedProperties) {
+    if (changedProperties.has('commentCount')) {
+      const level = Math.ceil(this.commentCount / 2);
+      this.activityLevel = Math.min(level, 3);
+    }
+    super.update(changedProperties);
+  }
+}
+customElements.define('mr-day-icon', MrDayIcon);
diff --git a/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js b/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js
new file mode 100644
index 0000000..3c35a10
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js
@@ -0,0 +1,24 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrDayIcon} from './mr-day-icon.js';
+
+
+let element;
+
+describe('mr-day-icon', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-day-icon');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrDayIcon);
+  });
+});
diff --git a/static_src/elements/chdir/mr-comment-table/mr-comment-table.js b/static_src/elements/chdir/mr-comment-table/mr-comment-table.js
new file mode 100644
index 0000000..a6d0f19
--- /dev/null
+++ b/static_src/elements/chdir/mr-comment-table/mr-comment-table.js
@@ -0,0 +1,130 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+import 'elements/framework/mr-comment-content/mr-comment-content.js';
+import 'elements/chops/chops-timestamp/chops-timestamp.js';
+
+/**
+ * `<mr-comment-table>`
+ *
+ * The list of comments for a Monorail Polymer profile.
+ *
+ */
+export class MrCommentTable extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      .ellipsis {
+        max-width: 50%;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+      }
+      table {
+        word-wrap: break-word;
+        width: 100%;
+      }
+      tr {
+        font-size: var(--chops-main-font-size);
+        font-weight: normal;
+        text-align: left;
+        line-height: 180%;
+      }
+      td, th {
+        border-bottom: var(--chops-normal-border);
+        padding: 0.25em 16px;
+      }
+      td {
+        text-overflow: ellipsis;
+      }
+      th {
+        text-align: left;
+      }
+      .no-wrap {
+        white-space: nowrap;
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    const comments = this._displayedComments(this.selectedDate, this.comments);
+    // TODO(zhangtiff): render deltas for comment changes.
+    return html`
+      <table cellspacing="0" cellpadding="0">
+        <tbody>
+           <tr id="heading-row">
+            <th>Date</th>
+            <th>Project</th>
+            <th>Comment</th>
+            <th>Issue Link</th>
+          </tr>
+
+          ${comments && comments.length ? comments.map((comment) => html`
+            <tr id="row">
+              <td class="no-wrap">
+                <chops-timestamp
+                  .timestamp=${comment.timestamp}
+                  short
+                ></chops-timestamp>
+              </td>
+              <td>${comment.projectName}</td>
+              <td class="ellipsis">
+                <mr-comment-content
+                  .content=${this._truncateMessage(comment.content)}
+                ></mr-comment-content>
+              </td>
+              <td class="no-wrap">
+                <a href="/p/${comment.projectName}/issues/detail?id=${comment.localId}">
+                  Issue ${comment.localId}
+                </a>
+              </td>
+            </tr>
+          `) : html`
+            <tr>
+              <td colspan="4"><i>No comments.</i></td>
+            </tr>
+          `}
+        </tbody>
+      </table>
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      comments: {type: Array},
+      selectedDate: {type: Number},
+    };
+  }
+
+  /** @override */
+  constructor() {
+    super();
+    this.comments = [];
+  }
+
+  _truncateMessage(message) {
+    return message && message.substring(0, message.indexOf('\n'));
+  }
+
+  _displayedComments(selectedDate, comments) {
+    if (!selectedDate) {
+      return comments;
+    } else {
+      const computedComments = [];
+      if (!comments) return computedComments;
+
+      for (let i = 0; i < comments.length; i++) {
+        if (comments[i].timestamp <= selectedDate &&
+           comments[i].timestamp >= (selectedDate - 86400)) {
+          computedComments.push(comments[i]);
+        }
+      }
+      return computedComments;
+    }
+  }
+}
+customElements.define('mr-comment-table', MrCommentTable);
diff --git a/static_src/elements/chdir/mr-comment-table/mr-comment-table.test.js b/static_src/elements/chdir/mr-comment-table/mr-comment-table.test.js
new file mode 100644
index 0000000..6925dc4
--- /dev/null
+++ b/static_src/elements/chdir/mr-comment-table/mr-comment-table.test.js
@@ -0,0 +1,24 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrCommentTable} from './mr-comment-table.js';
+
+
+let element;
+
+describe('mr-comment-table', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-comment-table');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrCommentTable);
+  });
+});
diff --git a/static_src/elements/chdir/mr-profile-page/mr-profile-page.js b/static_src/elements/chdir/mr-profile-page/mr-profile-page.js
new file mode 100644
index 0000000..5fadff6
--- /dev/null
+++ b/static_src/elements/chdir/mr-profile-page/mr-profile-page.js
@@ -0,0 +1,156 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+import {prpcClient} from 'prpc-client-instance.js';
+
+import 'elements/framework/mr-header/mr-header.js';
+import '../mr-activity-table/mr-activity-table.js';
+import '../mr-comment-table/mr-comment-table.js';
+
+/**
+ * `<mr-profile-page>`
+ *
+ * The main entry point for a Monorail web components profile.
+ *
+ */
+export class MrProfilePage extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      .history-container {
+        padding: 1em 16px;
+        display: flex;
+        flex-direction: column;
+        min-height: 100%;
+        box-sizing: border-box;
+        flex-grow: 1;
+      }
+      mr-comment-table {
+        width: 100%;
+        margin-bottom: 1em;
+        box-sizing: border-box;
+      }
+      mr-activity-table {
+        width: 70%;
+        flex-grow: 0;
+        margin: auto;
+        margin-bottom: 5em;
+        height: 200px;
+        box-sizing: border-box;
+      }
+      .metadata-container {
+        font-size: var(--chops-main-font-size);
+        border-right: var(--chops-normal-border);
+        width: 15%;
+        min-width: 256px;
+        flex-grow: 0;
+        flex-shrink: 0;
+        box-sizing: border-box;
+        min-height: 100%;
+      }
+      .container-outside {
+        box-sizing: border-box;
+        width: 100%;
+        max-width: 100%;
+        margin: auto;
+        padding: 0.75em 8px;
+        display: flex;
+        align-items: stretch;
+        justify-content: space-between;
+        flex-direction: row;
+        flex-wrap: no-wrap;
+        flex-grow: 0;
+        min-height: 100%;
+      }
+      .profile-data {
+        text-align: center;
+        padding-top: 40%;
+        font-size: var(--chops-main-font-size);
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <mr-header
+        .userDisplayName=${this.user}
+        .loginUrl=${this.loginUrl}
+        .logoutUrl=${this.logoutUrl}
+      >
+        <span slot="subheader">
+          &gt; Viewing Profile: ${this.viewedUser}
+        </span>
+      </mr-header>
+      <div class="container-outside">
+        <div class="metadata-container">
+          <div class="profile-data">
+            ${this.viewedUser} <br>
+            <b>Last visit:</b> ${this.lastVisitStr} <br>
+            <b>Starred Developers:</b>
+            ${this.starredUsers.length ? this.starredUsers.join(', ') : 'None'}
+          </div>
+        </div>
+        <div class="history-container">
+          ${this.user === this.viewedUser ? html`
+            <mr-activity-table
+              .comments=${this.comments}
+              @dateChange=${this._changeDate}
+            ></mr-activity-table>
+          `: ''}
+          <mr-comment-table
+            .user=${this.viewedUser}
+            .viewedUserId=${this.viewedUserId}
+            .comments=${this.comments}
+            .selectedDate=${this.selectedDate}>
+          </mr-comment-table>
+        </div>
+      </div>
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      user: {type: String},
+      logoutUrl: {type: String},
+      loginUrl: {type: String},
+      viewedUser: {type: String},
+      viewedUserId: {type: Number},
+      lastVisitStr: {type: String},
+      starredUsers: {type: Array},
+      comments: {type: Array},
+      selectedDate: {type: Number},
+    };
+  }
+
+  /** @override */
+  updated(changedProperties) {
+    if (changedProperties.has('viewedUserId')) {
+      this._fetchActivity();
+    }
+  }
+
+  async _fetchActivity() {
+    const commentMessage = {
+      userRef: {
+        userId: this.viewedUserId,
+      },
+    };
+
+    const resp = await prpcClient.call(
+        'monorail.Issues', 'ListActivities', commentMessage
+    );
+
+    this.comments = resp.comments;
+  }
+
+  _changeDate(e) {
+    if (!e.detail) return;
+    this.selectedDate = e.detail.date;
+  }
+}
+
+customElements.define('mr-profile-page', MrProfilePage);
diff --git a/static_src/elements/chdir/mr-profile-page/mr-profile-page.test.js b/static_src/elements/chdir/mr-profile-page/mr-profile-page.test.js
new file mode 100644
index 0000000..c967704
--- /dev/null
+++ b/static_src/elements/chdir/mr-profile-page/mr-profile-page.test.js
@@ -0,0 +1,24 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrProfilePage} from './mr-profile-page.js';
+
+
+let element;
+
+describe('mr-profile-page', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-profile-page');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrProfilePage);
+  });
+});