| /** |
| * @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 PATCH_SET_PREFIX_PATTERN = /^Patch Set (\d)+:[ ]?/; |
| const COMMENTS_COUNT_PATTERN = /^\((\d+)( inline)? comments?\)$/; |
| const LABEL_TITLE_SCORE_PATTERN = /^([A-Za-z0-9-]+)([+-]\d+)$/; |
| |
| 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, |
| /** @type {?} */ |
| message: Object, |
| author: { |
| type: Object, |
| computed: '_computeAuthor(message)', |
| }, |
| comments: { |
| type: Object, |
| observer: '_commentsChanged', |
| }, |
| config: Object, |
| hideAutomated: { |
| type: Boolean, |
| value: false, |
| }, |
| hidden: { |
| type: Boolean, |
| computed: '_computeIsHidden(hideAutomated, isAutomated)', |
| reflectToAttribute: true, |
| }, |
| isAutomated: { |
| type: Boolean, |
| computed: '_computeIsAutomated(message)', |
| }, |
| showAvatar: { |
| type: Boolean, |
| computed: '_computeShowAvatar(author, config)', |
| }, |
| showOnBehalfOf: { |
| type: Boolean, |
| computed: '_computeShowOnBehalfOf(message)', |
| }, |
| showReplyButton: { |
| type: Boolean, |
| computed: '_computeShowReplyButton(message, _loggedIn)', |
| }, |
| projectName: { |
| type: String, |
| observer: '_projectNameChanged', |
| }, |
| |
| /** |
| * A mapping from label names to objects representing the minimum and |
| * maximum possible values for that label. |
| */ |
| labelExtremes: Object, |
| |
| /** |
| * @type {{ commentlinks: Array }} |
| */ |
| _projectConfig: Object, |
| // Computed property needed to trigger Polymer value observing. |
| _expanded: { |
| type: Object, |
| computed: '_computeExpanded(message.expanded)', |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| _parsedPatchNum: String, |
| _parsedCommentCount: String, |
| _parsedVotes: Array, |
| _parsedChangeMessage: String, |
| _successfulParse: Boolean, |
| }, |
| |
| observers: [ |
| '_updateExpandedClass(message.expanded)', |
| '_consumeMessage(message.message)', |
| ], |
| |
| ready() { |
| this.$.restAPI.getConfig().then(config => { |
| this.config = config; |
| }); |
| this.$.restAPI.getLoggedIn().then(loggedIn => { |
| this._loggedIn = loggedIn; |
| }); |
| }, |
| |
| _updateExpandedClass(expanded) { |
| if (expanded) { |
| this.classList.add('expanded'); |
| } else { |
| this.classList.remove('expanded'); |
| } |
| }, |
| |
| _computeAuthor(message) { |
| return message.author || message.updated_by; |
| }, |
| |
| _computeShowAvatar(author, config) { |
| return !!(author && config && config.plugin && config.plugin.has_avatars); |
| }, |
| |
| _computeShowOnBehalfOf(message) { |
| const author = message.author || message.updated_by; |
| return !!(author && message.real_author && |
| author._account_id != message.real_author._account_id); |
| }, |
| |
| _computeShowReplyButton(message, loggedIn) { |
| return !!message.message && loggedIn && |
| !this._computeIsAutomated(message); |
| }, |
| |
| _computeExpanded(expanded) { |
| return expanded; |
| }, |
| |
| /** |
| * If there is no value set on the message object as to whether _expanded |
| * should be true or not, then _expanded is set to true if there are |
| * inline comments (otherwise false). |
| */ |
| _commentsChanged(value) { |
| if (this.message && this.message.expanded === undefined) { |
| this.set('message.expanded', Object.keys(value || {}).length > 0); |
| } |
| }, |
| |
| _handleTap(e) { |
| if (this.message.expanded) { return; } |
| e.stopPropagation(); |
| this.set('message.expanded', true); |
| }, |
| |
| _handleAuthorTap(e) { |
| if (!this.message.expanded) { return; } |
| e.stopPropagation(); |
| this.set('message.expanded', false); |
| }, |
| |
| _computeIsAutomated(message) { |
| return !!(message.reviewer || |
| this._computeIsReviewerUpdate(message) || |
| (message.tag && message.tag.startsWith('autogenerated'))); |
| }, |
| |
| _computeIsHidden(hideAutomated, isAutomated) { |
| return hideAutomated && isAutomated; |
| }, |
| |
| _computeIsReviewerUpdate(event) { |
| return event.type === 'REVIEWER_UPDATE'; |
| }, |
| |
| _computeScoreClass(score, labelExtremes) { |
| const classes = []; |
| if (score.value > 0) { |
| classes.push('positive'); |
| } else if (score.value < 0) { |
| classes.push('negative'); |
| } |
| const extremes = labelExtremes[score.label]; |
| if (extremes) { |
| const intScore = parseInt(score.value, 10); |
| if (intScore === extremes.max) { |
| classes.push('max'); |
| } else if (intScore === extremes.min) { |
| classes.push('min'); |
| } |
| } |
| return classes.join(' '); |
| }, |
| |
| _computeClass(expanded, showAvatar, message) { |
| const classes = []; |
| classes.push(expanded ? 'expanded' : 'collapsed'); |
| classes.push(showAvatar ? 'showAvatar' : 'hideAvatar'); |
| return classes.join(' '); |
| }, |
| |
| _computeMessageHash(message) { |
| return '#message-' + message.id; |
| }, |
| |
| _handleLinkTap(e) { |
| e.preventDefault(); |
| |
| this.fire('scroll-to', {message: this.message}, {bubbles: false}); |
| |
| const 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(e) { |
| e.preventDefault(); |
| this.fire('reply', {message: this.message}); |
| }, |
| |
| _projectNameChanged(name) { |
| this.$.restAPI.getProjectConfig(name).then(config => { |
| this._projectConfig = config; |
| }); |
| }, |
| |
| _computeExpandToggleIcon(expanded) { |
| return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more'; |
| }, |
| |
| _toggleExpanded(e) { |
| e.stopPropagation(); |
| this.set('message.expanded', !this.message.expanded); |
| }, |
| |
| /** |
| * Attempts to consume a change message to create a shorter and more legible |
| * format. If the function encounters unexpected characters at any point, it |
| * sets the _successfulParse flag to false and terminates, causing the UI to |
| * fall back to displaying the entirety of the change message. |
| * |
| * A successful parse results in a one-liner that reads: |
| * `${AVATAR} voted ${VOTES} and left ${NUM} comment(s) on ${PATCHSET}` |
| * |
| * @param {string} text |
| */ |
| _consumeMessage(text) { |
| this._parsedPatchNum = ''; |
| this._parsedCommentCount = ''; |
| this._parsedChangeMessage = ''; |
| this._parsedVotes = []; |
| if (!text) { |
| // No message body means nothing to parse. |
| this._successfulParse = false; |
| return; |
| } |
| const lines = text.split('\n'); |
| const messageLines = lines.shift().split(PATCH_SET_PREFIX_PATTERN); |
| if (!messageLines[1]) { |
| // Message is in an unexpected format. |
| this._successfulParse = false; |
| return; |
| } |
| this._parsedPatchNum = messageLines[1]; |
| if (messageLines[2]) { |
| // Content after the colon is always vote information. If it is in the |
| // most up to date schema, parse it. Otherwise, cancel the parsing |
| // completely. |
| let match; |
| for (const score of messageLines[2].split(' ')) { |
| match = score.match(LABEL_TITLE_SCORE_PATTERN); |
| if (!match || match.length !== 3) { |
| this._successfulParse = false; |
| return; |
| } |
| this._parsedVotes.push({label: match[1], value: match[2]}); |
| } |
| } |
| // Remove empty line. |
| lines.shift(); |
| if (lines.length) { |
| const commentMatch = lines[0].match(COMMENTS_COUNT_PATTERN); |
| if (commentMatch) { |
| this._parsedCommentCount = commentMatch[1]; |
| // Remove comment line and the following empty line. |
| lines.splice(0, 2); |
| } |
| this._parsedChangeMessage = lines.join('\n'); |
| } |
| this._successfulParse = true; |
| }, |
| |
| _computeConversationalString(votes, patchNum, commentCount) { |
| let clause = ' on Patch Set ' + patchNum; |
| if (commentCount) { |
| let commentStr = ' comment'; |
| if (parseInt(commentCount, 10) > 1) { commentStr += 's'; } |
| clause = ' left ' + commentCount + commentStr + clause; |
| if (votes.length) { clause = ' and' + clause; } |
| } |
| return clause; |
| }, |
| }); |
| })(); |