Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | // Copyright 2019 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import {LitElement, html, css} from 'lit-element'; |
| 6 | import qs from 'qs'; |
| 7 | import {store, connectStore} from 'reducers/base.js'; |
| 8 | import * as userV0 from 'reducers/userV0.js'; |
| 9 | import * as issueV0 from 'reducers/issueV0.js'; |
| 10 | import * as projectV0 from 'reducers/projectV0.js'; |
| 11 | import 'elements/chops/chops-button/chops-button.js'; |
| 12 | import 'elements/chops/chops-dialog/chops-dialog.js'; |
| 13 | import {SHARED_STYLES} from 'shared/shared-styles.js'; |
| 14 | import {cueNames} from './cue-helpers.js'; |
| 15 | |
| 16 | |
| 17 | /** |
| 18 | * `<mr-cue>` |
| 19 | * |
| 20 | * An element that displays one of a set of predefined help messages |
| 21 | * iff that message is appropriate to the current user and page. |
| 22 | * |
| 23 | * TODO: Factor this class out into a base view component and separate |
| 24 | * usage-specific components, such as those for user prefs. |
| 25 | * |
| 26 | */ |
| 27 | export class MrCue extends connectStore(LitElement) { |
| 28 | /** @override */ |
| 29 | constructor() { |
| 30 | super(); |
| 31 | this.prefs = new Map(); |
| 32 | this.issue = null; |
| 33 | this.referencedUsers = new Map(); |
| 34 | this.nondismissible = false; |
| 35 | this.cuePrefName = ''; |
| 36 | this.loginUrl = ''; |
| 37 | this.hidden = this._shouldBeHidden(this.signedIn, this.prefsLoaded, |
| 38 | this.cuePrefName, this.message); |
| 39 | } |
| 40 | |
| 41 | /** @override */ |
| 42 | static get properties() { |
| 43 | return { |
| 44 | issue: {type: Object}, |
| 45 | referencedUsers: {type: Object}, |
| 46 | user: {type: Object}, |
| 47 | cuePrefName: {type: String}, |
| 48 | nondismissible: {type: Boolean}, |
| 49 | prefs: {type: Object}, |
| 50 | prefsLoaded: {type: Boolean}, |
| 51 | jumpLocalId: {type: Number}, |
| 52 | loginUrl: {type: String}, |
| 53 | hidden: { |
| 54 | type: Boolean, |
| 55 | reflect: true, |
| 56 | }, |
| 57 | }; |
| 58 | } |
| 59 | |
| 60 | /** @override */ |
| 61 | static get styles() { |
| 62 | return [SHARED_STYLES, css` |
| 63 | :host { |
| 64 | display: block; |
| 65 | margin: 2px 0; |
| 66 | padding: 2px 4px 2px 8px; |
| 67 | background: var(--chops-notice-bubble-bg); |
| 68 | border: var(--chops-notice-border); |
| 69 | text-align: center; |
| 70 | } |
| 71 | :host([centered]) { |
| 72 | display: flex; |
| 73 | justify-content: center; |
| 74 | } |
| 75 | :host([hidden]) { |
| 76 | display: none; |
| 77 | } |
| 78 | button[hidden] { |
| 79 | visibility: hidden; |
| 80 | } |
| 81 | i.material-icons { |
| 82 | font-size: 14px; |
| 83 | } |
| 84 | button { |
| 85 | background: none; |
| 86 | border: none; |
| 87 | float: right; |
| 88 | padding: 2px; |
| 89 | cursor: pointer; |
| 90 | border-radius: 50%; |
| 91 | display: inline-flex; |
| 92 | align-items: center; |
| 93 | justify-content: center; |
| 94 | } |
| 95 | button:hover { |
| 96 | background: rgba(0, 0, 0, .2); |
| 97 | } |
| 98 | `]; |
| 99 | } |
| 100 | |
| 101 | /** @override */ |
| 102 | render() { |
| 103 | return html` |
| 104 | <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> |
| 105 | <button |
| 106 | @click=${this.dismiss} |
| 107 | title="Don't show this message again." |
| 108 | ?hidden=${this.nondismissible}> |
| 109 | <i class="material-icons">close</i> |
| 110 | </button> |
| 111 | <div id="message">${this.message}</div> |
| 112 | `; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * @return {TemplateResult} lit-html template for the cue message a user |
| 117 | * should see. |
| 118 | */ |
| 119 | get message() { |
| 120 | if (this.cuePrefName === cueNames.CODE_OF_CONDUCT) { |
| 121 | return html` |
| 122 | Please keep discussions respectful and constructive. |
| 123 | See our |
| 124 | <a href="${this.codeOfConductUrl}" |
| 125 | target="_blank">code of conduct</a>. |
| 126 | `; |
| 127 | } else if (this.cuePrefName === cueNames.AVAILABILITY_MSGS) { |
| 128 | if (this._availablityMsgsRelevant(this.issue)) { |
| 129 | return html` |
| 130 | <b>Note:</b> |
| 131 | Clock icons indicate that users may not be available. |
| 132 | Tooltips show the reason. |
| 133 | `; |
| 134 | } |
| 135 | } else if (this.cuePrefName === cueNames.SWITCH_TO_PARENT_ACCOUNT) { |
| 136 | if (this._switchToParentAccountRelevant()) { |
| 137 | return html` |
| 138 | You are signed in to a linked account. |
| 139 | <a href="${this.loginUrl}"> |
| 140 | Switch to ${this.user.linkedParentRef.displayName}</a>. |
| 141 | `; |
| 142 | } |
| 143 | } else if (this.cuePrefName === cueNames.SEARCH_FOR_NUMBERS) { |
| 144 | if (this._searchForNumbersRelevant(this.jumpLocalId)) { |
| 145 | return html` |
| 146 | <b>Tip:</b> |
| 147 | To find issues containing "${this.jumpLocalId}", use quotes. |
| 148 | `; |
| 149 | } |
| 150 | } |
| 151 | return; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Conditionally returns a hardcoded code of conduct URL for |
| 156 | * different projects. |
| 157 | * @return {string} the URL for the code of conduct. |
| 158 | */ |
| 159 | get codeOfConductUrl() { |
| 160 | // TODO(jrobbins): Store this in the DB and pass it via the API. |
| 161 | if (this.projectName === 'fuchsia') { |
| 162 | return 'https://fuchsia.dev/fuchsia-src/CODE_OF_CONDUCT'; |
| 163 | } |
| 164 | return ('https://chromium.googlesource.com/' + |
| 165 | 'chromium/src/+/main/CODE_OF_CONDUCT.md'); |
| 166 | } |
| 167 | |
| 168 | /** @override */ |
| 169 | updated(changedProperties) { |
| 170 | const hiddenWatchProps = ['prefsLoaded', 'cuePrefName', 'signedIn', |
| 171 | 'prefs']; |
| 172 | const shouldUpdateHidden = Array.from(changedProperties.keys()) |
| 173 | .some((propName) => hiddenWatchProps.includes(propName)); |
| 174 | if (shouldUpdateHidden) { |
| 175 | this.hidden = this._shouldBeHidden(this.signedIn, this.prefsLoaded, |
| 176 | this.cuePrefName, this.message); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Checks if there are any unavailable users and only displays this cue if so. |
| 182 | * @param {Issue} issue |
| 183 | * @return {boolean} Whether the User Availability cue should be |
| 184 | * displayed or not. |
| 185 | */ |
| 186 | _availablityMsgsRelevant(issue) { |
| 187 | if (!issue) return false; |
| 188 | return (this._anyUnvailable([issue.ownerRef]) || |
| 189 | this._anyUnvailable(issue.ccRefs)); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Checks if a given list of users contains any unavailable users. |
| 194 | * @param {Array<UserRef>} userRefList |
| 195 | * @return {boolean} Whether there are unavailable users. |
| 196 | */ |
| 197 | _anyUnvailable(userRefList) { |
| 198 | if (!userRefList) return false; |
| 199 | for (const userRef of userRefList) { |
| 200 | if (userRef) { |
| 201 | const participant = this.referencedUsers.get(userRef.displayName); |
| 202 | if (participant && participant.availability) return true; |
| 203 | } |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Finds if the user has a linked parent account that's separate from the |
| 209 | * one they are logged into and conditionally hides the cue if so. |
| 210 | * @return {boolean} Whether to show the cue to switch to a parent account. |
| 211 | */ |
| 212 | _switchToParentAccountRelevant() { |
| 213 | return this.user && this.user.linkedParentRef; |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Determines whether the user should see a cue telling them how to avoid the |
| 218 | * "jump to issue" feature. |
| 219 | * @param {number} jumpLocalId the ID of the issue the user jumped to. |
| 220 | * @return {boolean} Whether the user jumped to a number or not. |
| 221 | */ |
| 222 | _searchForNumbersRelevant(jumpLocalId) { |
| 223 | return !!jumpLocalId; |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Checks the user's preferences to hide a particular cue if they have |
| 228 | * dismissed it. |
| 229 | * @param {boolean} signedIn Whether the user is signed in. |
| 230 | * @param {boolean} prefsLoaded Whether the user's prefs have been fetched |
| 231 | * from the API. |
| 232 | * @param {string} cuePrefName The name of the cue being checked. |
| 233 | * @param {string} message |
| 234 | * @return {boolean} Whether the cue should be hidden. |
| 235 | */ |
| 236 | _shouldBeHidden(signedIn, prefsLoaded, cuePrefName, message) { |
| 237 | if (signedIn && !prefsLoaded) return true; |
| 238 | if (this.alreadyDismissed(cuePrefName)) return true; |
| 239 | return !message; |
| 240 | } |
| 241 | |
| 242 | /** @override */ |
| 243 | stateChanged(state) { |
| 244 | this.projectName = projectV0.viewedProjectName(state); |
| 245 | this.issue = issueV0.viewedIssue(state); |
| 246 | this.referencedUsers = issueV0.referencedUsers(state); |
| 247 | this.user = userV0.currentUser(state); |
| 248 | this.prefs = userV0.prefs(state); |
| 249 | this.signedIn = this.user && this.user.userId; |
| 250 | this.prefsLoaded = userV0.currentUser(state).prefsLoaded; |
| 251 | |
| 252 | const queryString = window.location.search.substring(1); |
| 253 | const queryParams = qs.parse(queryString); |
| 254 | const q = queryParams.q; |
| 255 | if (q && q.match(new RegExp('^\\d+$'))) { |
| 256 | this.jumpLocalId = Number(q); |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | /** |
| 261 | * Check whether a cue has already been dismissed in a user's |
| 262 | * preferences. |
| 263 | * @param {string} pref The name of the user preference to check. |
| 264 | * @return {boolean} Whether the cue was dismissed or not. |
| 265 | */ |
| 266 | alreadyDismissed(pref) { |
| 267 | return this.prefs && this.prefs.get(pref); |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Sends a request to the API to save that a user has dismissed a cue. |
| 272 | * The results of this request update Redux's state, which leads to |
| 273 | * the cue disappearing for the user after the request finishes. |
| 274 | * @return {void} |
| 275 | */ |
| 276 | dismiss() { |
| 277 | const newPrefs = [{name: this.cuePrefName, value: 'true'}]; |
| 278 | store.dispatch(userV0.setPrefs(newPrefs, this.signedIn)); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | customElements.define('mr-cue', MrCue); |