Moment.js has a large bundle size and it causes a performance overhead. Change-Id: Ida5c2642006d5018300527cb2b5eaab3c61772d2
		
			
				
	
	
		
			376 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			376 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright (C) 2020 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.
 | 
						|
 */
 | 
						|
import '@polymer/paper-toggle-button/paper-toggle-button.js';
 | 
						|
import '../../shared/gr-button/gr-button.js';
 | 
						|
import '../../shared/gr-icons/gr-icons.js';
 | 
						|
import '../gr-message/gr-message.js';
 | 
						|
import '../../../styles/shared-styles.js';
 | 
						|
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 | 
						|
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 | 
						|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 | 
						|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 | 
						|
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
						|
import {htmlTemplate} from './gr-messages-list-experimental_html.js';
 | 
						|
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 | 
						|
import {parseDate} from '../../../utils/date-util.js';
 | 
						|
import {MessageTag} from '../../../constants/constants.js';
 | 
						|
import {appContext} from '../../../services/app-context.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * The content of the enum is also used in the UI for the button text.
 | 
						|
 *
 | 
						|
 * @enum {string}
 | 
						|
 */
 | 
						|
const ExpandAllState = {
 | 
						|
  EXPAND_ALL: 'Expand All',
 | 
						|
  COLLAPSE_ALL: 'Collapse All',
 | 
						|
};
 | 
						|
 | 
						|
function isNewPatchSet(message) {
 | 
						|
  if (!message || !message.tag) return false;
 | 
						|
  return message.tag.includes(MessageTag.TAG_NEW_PATCHSET)
 | 
						|
      || message.tag.includes(MessageTag.TAG_NEW_WIP_PATCHSET);
 | 
						|
}
 | 
						|
 | 
						|
function hasHigherRevisionNumber(m, message) {
 | 
						|
  return m._revision_number && m._revision_number > message._revision_number;
 | 
						|
}
 | 
						|
 | 
						|
function isNewerReviewerUpdate(m, message) {
 | 
						|
  if (!message || !message.tag || !m || !m.tag) return false;
 | 
						|
  if (m.tag != MessageTag.TAG_REVIEWER_UPDATE) return false;
 | 
						|
  return m.date > message.date;
 | 
						|
}
 | 
						|
 | 
						|
function isImportant(message, allMessages) {
 | 
						|
  // Human messages don't have a tag. They are always important.
 | 
						|
  if (!message.tag) return true;
 | 
						|
 | 
						|
  // Autogenerated messages are only important, if there is not a newer message
 | 
						|
  // with the same tag.
 | 
						|
  const tag = message.tag;
 | 
						|
  const sameTag = m =>
 | 
						|
    m.tag === tag || (isNewPatchSet(m) && isNewPatchSet(message));
 | 
						|
  return !allMessages.filter(sameTag).some(m =>
 | 
						|
    hasHigherRevisionNumber(m, message) || isNewerReviewerUpdate(m, message));
 | 
						|
}
 | 
						|
 | 
						|
export const TEST_ONLY = {
 | 
						|
  isImportant,
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * @extends PolymerElement
 | 
						|
 */
 | 
						|
class GrMessagesListExperimental extends mixinBehaviors( [
 | 
						|
  KeyboardShortcutBehavior,
 | 
						|
], GestureEventListeners(
 | 
						|
    LegacyElementMixin(
 | 
						|
        PolymerElement))) {
 | 
						|
  static get template() { return htmlTemplate; }
 | 
						|
 | 
						|
  static get is() { return 'gr-messages-list-experimental'; }
 | 
						|
 | 
						|
  static get properties() {
 | 
						|
    return {
 | 
						|
      /** @type {?} */
 | 
						|
      change: Object,
 | 
						|
      changeNum: Number,
 | 
						|
      /**
 | 
						|
       * These are just the change messages. They are combined with reviewer
 | 
						|
       * updates below. So _combinedMessages is the more important property.
 | 
						|
       */
 | 
						|
      messages: {
 | 
						|
        type: Array,
 | 
						|
        value() { return []; },
 | 
						|
      },
 | 
						|
      /**
 | 
						|
       * These are just the reviewer updates. They are combined with change
 | 
						|
       * messages above. So _combinedMessages is the more important property.
 | 
						|
       */
 | 
						|
      reviewerUpdates: {
 | 
						|
        type: Array,
 | 
						|
        value() { return []; },
 | 
						|
      },
 | 
						|
      changeComments: Object,
 | 
						|
      projectName: String,
 | 
						|
      showReplyButtons: {
 | 
						|
        type: Boolean,
 | 
						|
        value: false,
 | 
						|
      },
 | 
						|
      labels: Object,
 | 
						|
 | 
						|
      /**
 | 
						|
       * Keeps track of the state of the "Expand All" toggle button. Note that
 | 
						|
       * you can individually expand/collapse some messages without affecting
 | 
						|
       * the toggle button's state.
 | 
						|
       *
 | 
						|
       * @type {ExpandAllState}
 | 
						|
       */
 | 
						|
      _expandAllState: {
 | 
						|
        type: String,
 | 
						|
        value: ExpandAllState.EXPAND_ALL,
 | 
						|
      },
 | 
						|
      _expandAllTitle: {
 | 
						|
        type: String,
 | 
						|
        computed: '_computeExpandAllTitle(_expandAllState)',
 | 
						|
      },
 | 
						|
 | 
						|
      _showAllActivity: {
 | 
						|
        type: Boolean,
 | 
						|
        value: false,
 | 
						|
        observer: '_observeShowAllActivity',
 | 
						|
      },
 | 
						|
      /**
 | 
						|
       * The merged array of change messages and reviewer updates.
 | 
						|
       */
 | 
						|
      _combinedMessages: {
 | 
						|
        type: Array,
 | 
						|
        computed: '_computeCombinedMessages(messages, reviewerUpdates)',
 | 
						|
        observer: '_combinedMessagesChanged',
 | 
						|
      },
 | 
						|
 | 
						|
      _labelExtremes: {
 | 
						|
        type: Object,
 | 
						|
        computed: '_computeLabelExtremes(labels.*)',
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  constructor() {
 | 
						|
    super();
 | 
						|
    this.reporting = appContext.reportingService;
 | 
						|
  }
 | 
						|
 | 
						|
  scrollToMessage(messageID) {
 | 
						|
    const selector = `[data-message-id="${messageID}"]`;
 | 
						|
    const el = this.shadowRoot.querySelector(selector);
 | 
						|
 | 
						|
    if (!el && this._showAllActivity) {
 | 
						|
      console.warn(`Failed to scroll to message: ${messageID}`);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (!el) {
 | 
						|
      this._showAllActivity = true;
 | 
						|
      setTimeout(() => this.scrollToMessage(messageID));
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    el.set('message.expanded', true);
 | 
						|
    let top = el.offsetTop;
 | 
						|
    for (let offsetParent = el.offsetParent;
 | 
						|
      offsetParent;
 | 
						|
      offsetParent = offsetParent.offsetParent) {
 | 
						|
      top += offsetParent.offsetTop;
 | 
						|
    }
 | 
						|
    window.scrollTo(0, top);
 | 
						|
    this._highlightEl(el);
 | 
						|
  }
 | 
						|
 | 
						|
  _observeShowAllActivity(showAllActivity) {
 | 
						|
    // We have to call render() such that the dom-repeat filter picks up the
 | 
						|
    // change.
 | 
						|
    this.$.messageRepeat.render();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Filter for the dom-repeat of combinedMessages.
 | 
						|
   */
 | 
						|
  _isMessageVisible(message) {
 | 
						|
    const allMessages = this._combinedMessages;
 | 
						|
    return this._showAllActivity || isImportant(message, allMessages);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Merges change messages and reviewer updates into one array.
 | 
						|
   */
 | 
						|
  _computeCombinedMessages(messages, reviewerUpdates) {
 | 
						|
    messages = messages || [];
 | 
						|
    reviewerUpdates = reviewerUpdates || [];
 | 
						|
    let mi = 0;
 | 
						|
    let ri = 0;
 | 
						|
    let combinedMessages = [];
 | 
						|
    let mDate;
 | 
						|
    let rDate;
 | 
						|
    for (let i = 0; i < messages.length; i++) {
 | 
						|
      messages[i]._index = i;
 | 
						|
    }
 | 
						|
 | 
						|
    while (mi < messages.length || ri < reviewerUpdates.length) {
 | 
						|
      if (mi >= messages.length) {
 | 
						|
        combinedMessages = combinedMessages.concat(reviewerUpdates.slice(ri));
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      if (ri >= reviewerUpdates.length) {
 | 
						|
        combinedMessages = combinedMessages.concat(messages.slice(mi));
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      mDate = mDate || parseDate(messages[mi].date);
 | 
						|
      rDate = rDate || parseDate(reviewerUpdates[ri].date);
 | 
						|
      if (rDate < mDate) {
 | 
						|
        combinedMessages.push(reviewerUpdates[ri++]);
 | 
						|
        rDate = null;
 | 
						|
      } else {
 | 
						|
        combinedMessages.push(messages[mi++]);
 | 
						|
        mDate = null;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    combinedMessages.forEach(m => {
 | 
						|
      if (m.expanded === undefined) {
 | 
						|
        m.expanded = false;
 | 
						|
      }
 | 
						|
    });
 | 
						|
    return combinedMessages;
 | 
						|
  }
 | 
						|
 | 
						|
  _updateExpandedStateOfAllMessages(exp) {
 | 
						|
    if (this._combinedMessages) {
 | 
						|
      for (let i = 0; i < this._combinedMessages.length; i++) {
 | 
						|
        this._combinedMessages[i].expanded = exp;
 | 
						|
        this.notifyPath(`_combinedMessages.${i}.expanded`);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _computeExpandAllTitle(_expandAllState) {
 | 
						|
    if (_expandAllState === ExpandAllState.COLLAPSED_ALL) {
 | 
						|
      return this.createTitle(
 | 
						|
          this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
 | 
						|
    }
 | 
						|
    if (_expandAllState === ExpandAllState.EXPAND_ALL) {
 | 
						|
      return this.createTitle(
 | 
						|
          this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
 | 
						|
    }
 | 
						|
    return '';
 | 
						|
  }
 | 
						|
 | 
						|
  _highlightEl(el) {
 | 
						|
    const highlightedEls =
 | 
						|
        dom(this.root).querySelectorAll('.highlighted');
 | 
						|
    for (const highlightedEl of highlightedEls) {
 | 
						|
      highlightedEl.classList.remove('highlighted');
 | 
						|
    }
 | 
						|
    function handleAnimationEnd() {
 | 
						|
      el.removeEventListener('animationend', handleAnimationEnd);
 | 
						|
      el.classList.remove('highlighted');
 | 
						|
    }
 | 
						|
    el.addEventListener('animationend', handleAnimationEnd);
 | 
						|
    el.classList.add('highlighted');
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {boolean} expand
 | 
						|
   */
 | 
						|
  handleExpandCollapse(expand) {
 | 
						|
    this._expandAllState = expand ? ExpandAllState.COLLAPSE_ALL
 | 
						|
      : ExpandAllState.EXPAND_ALL;
 | 
						|
    this._updateExpandedStateOfAllMessages(expand);
 | 
						|
  }
 | 
						|
 | 
						|
  _handleExpandCollapseTap(e) {
 | 
						|
    e.preventDefault();
 | 
						|
    this.handleExpandCollapse(
 | 
						|
        this._expandAllState === ExpandAllState.EXPAND_ALL);
 | 
						|
  }
 | 
						|
 | 
						|
  _handleAnchorClick(e) {
 | 
						|
    this.scrollToMessage(e.detail.id);
 | 
						|
  }
 | 
						|
 | 
						|
  _isVisibleShowAllActivityToggle(messages) {
 | 
						|
    messages = messages || [];
 | 
						|
    return messages.some(m => !isImportant(m, messages));
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This method is for reporting stats only.
 | 
						|
   */
 | 
						|
  _combinedMessagesChanged(combinedMessages) {
 | 
						|
    if (combinedMessages) {
 | 
						|
      if (combinedMessages.length === 0) return;
 | 
						|
      const tags = combinedMessages.map(
 | 
						|
          message => message.tag || message.type ||
 | 
						|
              (message.comments ? 'comments' : 'none'));
 | 
						|
      const tagsCounted = tags.reduce((acc, val) => {
 | 
						|
        acc[val] = (acc[val] || 0) + 1;
 | 
						|
        return acc;
 | 
						|
      }, {all: combinedMessages.length});
 | 
						|
      this.reporting.reportInteraction('messages-count', tagsCounted);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Compute a mapping from label name to objects representing the minimum and
 | 
						|
   * maximum possible values for that label.
 | 
						|
   */
 | 
						|
  _computeLabelExtremes(labelRecord) {
 | 
						|
    const extremes = {};
 | 
						|
    const labels = labelRecord.base;
 | 
						|
    if (!labels) { return extremes; }
 | 
						|
    for (const key of Object.keys(labels)) {
 | 
						|
      if (!labels[key] || !labels[key].values) { continue; }
 | 
						|
      const values = Object.keys(labels[key].values)
 | 
						|
          .map(v => parseInt(v, 10));
 | 
						|
      values.sort((a, b) => a - b);
 | 
						|
      if (!values.length) { continue; }
 | 
						|
      extremes[key] = {min: values[0], max: values[values.length - 1]};
 | 
						|
    }
 | 
						|
    return extremes;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Computes message author's file comments for change's message. The backend
 | 
						|
   * sets comment.change_message_id for matching, so this computation is fairly
 | 
						|
   * straightforward.
 | 
						|
   *
 | 
						|
   * @param {!Object} changeComments changeComment object, which includes
 | 
						|
   *     a method to get all published comments (including robot comments),
 | 
						|
   *     which returns a Hash of arrays of comments, filename as key.
 | 
						|
   * @param {!Object} message
 | 
						|
   * @return {!Array} Array of comment threads.
 | 
						|
   */
 | 
						|
  _computeThreadsForMessage(changeComments, message) {
 | 
						|
    if ([changeComments, message].some(arg => arg === undefined)) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    if (message._index === undefined || !this.messages) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    const commentThreads = changeComments.getAllThreadsForChange();
 | 
						|
 | 
						|
    return commentThreads.filter(thread => thread.comments
 | 
						|
        .map(comment => {
 | 
						|
          // collapse all by default
 | 
						|
          comment.collapsed = true;
 | 
						|
          return comment;
 | 
						|
        }).some(comment => {
 | 
						|
          const condition = comment.change_message_id === message.id;
 | 
						|
          // Since getAllThreadsForChange() always returns a new copy of
 | 
						|
          // all comments we can modify them here without worrying about
 | 
						|
          // polluting other threads.
 | 
						|
          comment.collapsed = !condition;
 | 
						|
          return condition;
 | 
						|
        })
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
customElements.define(GrMessagesListExperimental.is,
 | 
						|
    GrMessagesListExperimental);
 |