/** * @license * 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'; const MAX_INITIAL_SHOWN_MESSAGES = 20; const MESSAGES_INCREMENT = 5; const ReportingEvent = { SHOW_ALL: 'show-all-messages', SHOW_MORE: 'show-more-messages', }; class GrMessagesList extends Polymer.GestureEventListeners( Polymer.LegacyElementMixin( Polymer.Element)) { static get is() { return 'gr-messages-list'; } static get properties() { return { changeNum: Number, messages: { type: Array, value() { return []; }, }, reviewerUpdates: { type: Array, value() { return []; }, }, changeComments: Object, projectName: String, showReplyButtons: { type: Boolean, value: false, }, labels: Object, _expanded: { type: Boolean, value: false, observer: '_expandedChanged', }, _hideAutomated: { type: Boolean, value: false, }, /** * The messages after processing and including merged reviewer updates. */ _processedMessages: { type: Array, computed: '_computeItems(messages, reviewerUpdates)', observer: '_processedMessagesChanged', }, /** * The subset of _processedMessages that is visible to the user. */ _visibleMessages: { type: Array, value() { return []; }, }, _labelExtremes: { type: Object, computed: '_computeLabelExtremes(labels.*)', }, }; } scrollToMessage(messageID) { let el = this.$$('[data-message-id="' + messageID + '"]'); // If the message is hidden, expand the hidden messages back to that // point. if (!el) { let index; for (index = 0; index < this._processedMessages.length; index++) { if (this._processedMessages[index].id === messageID) { break; } } if (index === this._processedMessages.length) { return; } const newMessages = this._processedMessages.slice(index, -this._visibleMessages.length); // Add newMessages to the beginning of _visibleMessages. this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); // Allow the dom-repeat to stamp. Polymer.dom.flush(); el = this.$$('[data-message-id="' + messageID + '"]'); } 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); } _isAutomated(message) { return !!(message.reviewer || (message.tag && message.tag.startsWith('autogenerated'))); } _computeItems(messages, reviewerUpdates) { // Polymer 2: check for undefined if ([messages, reviewerUpdates].some(arg => arg === undefined)) { return []; } messages = messages || []; reviewerUpdates = reviewerUpdates || []; let mi = 0; let ri = 0; let result = []; 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) { result = result.concat(reviewerUpdates.slice(ri)); break; } if (ri >= reviewerUpdates.length) { result = result.concat(messages.slice(mi)); break; } mDate = mDate || util.parseDate(messages[mi].date); rDate = rDate || util.parseDate(reviewerUpdates[ri].date); if (rDate < mDate) { result.push(reviewerUpdates[ri++]); rDate = null; } else { result.push(messages[mi++]); mDate = null; } } return result; } _expandedChanged(exp) { if (this._processedMessages) { for (let i = 0; i < this._processedMessages.length; i++) { this._processedMessages[i].expanded = exp; } } // _visibleMessages is a subarray of _processedMessages // _processedMessages contains all items from _visibleMessages // At this point all _visibleMessages.expanded values are set, // and notifyPath must be used to notify Polymer about changes. if (this._visibleMessages) { for (let i = 0; i < this._visibleMessages.length; i++) { this.notifyPath(`_visibleMessages.${i}.expanded`); } } } _highlightEl(el) { const highlightedEls = Polymer.dom(this.root).querySelectorAll('.highlighted'); for (const highlighedEl of highlightedEls) { highlighedEl.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._expanded = expand; } _handleExpandCollapseTap(e) { e.preventDefault(); this.handleExpandCollapse(!this._expanded); } _handleAnchorClick(e) { this.scrollToMessage(e.detail.id); } _hasAutomatedMessages(messages) { if (!messages) { return false; } for (const message of messages) { if (this._isAutomated(message)) { return true; } } return false; } _computeExpandCollapseMessage(expanded) { return expanded ? 'Collapse all' : 'Expand all'; } /** * Computes message author's file comments for change's message. * Method uses this.messages to find next message and relies on messages * to be sorted by date field descending. * @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 {!Object} Hash of arrays of comments, filename as key. */ _computeCommentsForMessage(changeComments, message) { if ([changeComments, message].some(arg => arg === undefined)) { return []; } const comments = changeComments.getAllPublishedComments(); if (message._index === undefined || !comments || !this.messages) { return []; } const messages = this.messages || []; const index = message._index; const authorId = message.author && message.author._account_id; const mDate = util.parseDate(message.date).getTime(); // NB: Messages array has oldest messages first. let nextMDate; if (index > 0) { for (let i = index - 1; i >= 0; i--) { if (messages[i] && messages[i].author && messages[i].author._account_id === authorId) { nextMDate = util.parseDate(messages[i].date).getTime(); break; } } } const msgComments = {}; for (const file in comments) { if (!comments.hasOwnProperty(file)) { continue; } const fileComments = comments[file]; for (let i = 0; i < fileComments.length; i++) { if (fileComments[i].author && fileComments[i].author._account_id !== authorId) { continue; } const cDate = util.parseDate(fileComments[i].updated).getTime(); if (cDate <= mDate) { if (nextMDate && cDate <= nextMDate) { continue; } msgComments[file] = msgComments[file] || []; msgComments[file].push(fileComments[i]); } } } return msgComments; } /** * Returns the number of messages to splice to the beginning of * _visibleMessages. This is the minimum of the total number of messages * remaining in the list and the number of messages needed to display five * more visible messages in the list. */ _getDelta(visibleMessages, messages, hideAutomated) { if ([visibleMessages, messages].some(arg => arg === undefined)) { return 0; } let delta = MESSAGES_INCREMENT; const msgsRemaining = messages.length - visibleMessages.length; if (hideAutomated) { let counter = 0; let i; for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) { if (!this._isAutomated(messages[i - 1])) { counter++; } } delta = msgsRemaining - i; } return Math.min(msgsRemaining, delta); } /** * Gets the number of messages that would be visible, but do not currently * exist in _visibleMessages. */ _numRemaining(visibleMessages, messages, hideAutomated) { if ([visibleMessages, messages].some(arg => arg === undefined)) { return 0; } if (hideAutomated) { return this._getHumanMessages(messages).length - this._getHumanMessages(visibleMessages).length; } return messages.length - visibleMessages.length; } _computeIncrementText(visibleMessages, messages, hideAutomated) { let delta = this._getDelta(visibleMessages, messages, hideAutomated); delta = Math.min( this._numRemaining(visibleMessages, messages, hideAutomated), delta); return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more'; } _getHumanMessages(messages) { return messages.filter(msg => { return !this._isAutomated(msg); }); } _computeShowHideTextHidden(visibleMessages, messages, hideAutomated) { if ([visibleMessages, messages].some(arg => arg === undefined)) { return 0; } if (hideAutomated) { messages = this._getHumanMessages(messages); visibleMessages = this._getHumanMessages(visibleMessages); } return visibleMessages.length >= messages.length; } _handleShowAllTap() { this._visibleMessages = this._processedMessages; this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL); } _handleIncrementShownMessages() { const delta = this._getDelta(this._visibleMessages, this._processedMessages, this._hideAutomated); const len = this._visibleMessages.length; const newMessages = this._processedMessages.slice(-(len + delta), -len); // Add newMessages to the beginning of _visibleMessages this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE); } _processedMessagesChanged(messages) { if (messages) { this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES); } } _computeNumMessagesText(visibleMessages, messages, hideAutomated) { const total = this._numRemaining(visibleMessages, messages, hideAutomated); return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages'; } _computeIncrementHidden(visibleMessages, messages, hideAutomated) { const total = this._numRemaining(visibleMessages, messages, hideAutomated); return total <= this._getDelta(visibleMessages, messages, hideAutomated); } /** * 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; } } customElements.define(GrMessagesList.is, GrMessagesList); })();