Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/static_src/elements/chops/chops-announcement/chops-announcement.js b/static_src/elements/chops/chops-announcement/chops-announcement.js
index 477e7d2..5d5a9d6 100644
--- a/static_src/elements/chops/chops-announcement/chops-announcement.js
+++ b/static_src/elements/chops/chops-announcement/chops-announcement.js
@@ -1,8 +1,13 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
+// Copyright 2020 The Chromium Authors
 // 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 { LitElement, html, css } from 'lit-element';
+import 'elements/framework/mr-comment-content/mr-comment-content.js';
+
+import { connectStore } from 'reducers/base.js';
+import * as projectV0 from 'reducers/projectV0.js';
+import * as userV0 from 'reducers/userV0.js';
 
 // URL where announcements are fetched from.
 const ANNOUNCEMENT_SERVICE =
@@ -20,9 +25,25 @@
 export const REFRESH_TIME_MS = 5 * 60 * 1000;
 
 /**
+ * @type {Array<Announcement>} A list of hardcodded announcements for Monorail.
+ */
+export const HARDCODED_ANNOUNCEMENTS = [{
+  "messageContent": "The Chromium project will be migrating to Buganizer on " +
+    " February 5  (go/chrome-buganizer). Please test your workflows for this " +
+    "transition with these instructions: go/cob-buv-quick-start",
+  "projects": ["chromium"],
+  "groups": ["everyone@google.com", "googlers@chromium.org"],
+}];
+
+/**
  * @typedef {Object} Announcement
- * @property {string} id
+ * @property {string=} id
  * @property {string} messageContent
+ * @property {Array<string>=} projects Monorail extension for hard-coded
+ *    announcements. Specifies the names of projects the announcement will
+ *    occur in.
+ * @property {Array<string>=} groups Monorail extension for hard-coded
+ *    announcements. Specifies email groups the announces will show up in.
  */
 
 /**
@@ -36,7 +57,7 @@
  *
  * @customElement chops-announcement
  */
-export class ChopsAnnouncement extends LitElement {
+class _ChopsAnnouncement extends LitElement {
   /** @override */
   static get styles() {
     return css`
@@ -44,7 +65,7 @@
         display: block;
         width: 100%;
       }
-      p {
+      mr-comment-content {
         display: block;
         color: #222;
         font-size: 13px;
@@ -65,17 +86,29 @@
       return html`<p><strong>Error: </strong>${this._error}</p>`;
     }
     return html`
-      ${this._announcements.map(
-      ({messageContent}) => html`<p>${messageContent}</p>`)}
+      ${this._processedAnnouncements().map(
+      ({ messageContent }) => html`
+          <mr-comment-content
+            .content=${messageContent}>
+          </mr-comment-content>`)}
     `;
   }
 
   /** @override */
   static get properties() {
     return {
-      service: {type: String},
-      _error: {type: String},
-      _announcements: {type: Array},
+      service: { type: String },
+      additionalAnnouncements: { type: Array },
+
+      // Properties from the currently logged in user, usually feched through
+      // Redux.
+      currentUserName: { type: String },
+      userGroups: { type: Array },
+      currentProject: { type: String },
+
+      // Private properties managing state from requests to Chops Dash.
+      _error: { type: String },
+      _announcements: { type: Array },
     };
   }
 
@@ -85,6 +118,13 @@
 
     /** @type {string} */
     this.service = undefined;
+    /** @type {Array<Announcement>} */
+    this.additionalAnnouncements = HARDCODED_ANNOUNCEMENTS;
+
+    this.currentUserName = '';
+    this.userGroups = [];
+    this.currentProject = '';
+
     /** @type {string} */
     this._error = undefined;
     /** @type {Array<Announcement>} */
@@ -135,12 +175,12 @@
    */
   async refresh() {
     try {
-      const {announcements = []} = await this.fetch(this.service);
+      const { announcements = [] } = await this.fetch(this.service);
       this._error = undefined;
       this._announcements = announcements;
     } catch (e) {
       this._error = e.message;
-      this._announcements = [];
+      this._announcements = HARDCODED_ANNOUNCEMENTS;
     }
   }
 
@@ -176,6 +216,64 @@
 
     return JSON.parse(text.substr(XSSI_PREFIX.length));
   }
+
+  _processedAnnouncements() {
+    const announcements = [...this.additionalAnnouncements, ...this._announcements];
+
+    // Only show announcements relevant to the project the user is viewing and
+    // the group the user is part of, if applicable.
+    return announcements.filter(({ groups, projects }) => {
+      if (groups && groups.length && !this._isUserInGroups(groups,
+        this.userGroups, this.currentUserName)) {
+        return false;
+      }
+      if (projects && projects.length && !this._isViewingProject(projects,
+        this.currentProject)) {
+        return false;
+      }
+      return true;
+    });
+  }
+
+  /**
+   * Helper to check if the user is a member of the allowed groups.
+   * @param {Array<string>} allowedGroups
+   * @param {Array<{{userId: string, displayName: string}}>} userGroups
+   * @param {string} userEmail
+   */
+  _isUserInGroups(allowedGroups, userGroups, userEmail) {
+    const userGroupSet = new Set(userGroups.map(
+        ({ displayName }) => displayName.toLowerCase()));
+    return allowedGroups.find((group) => {
+      group = group.toLowerCase();
+
+      // Handle custom groups in Monorail like everyone@google.com
+      if (group.startsWith('everyone@')) {
+        let [_, suffix] = group.split('@');
+        suffix = '@' + suffix;
+        return userEmail.endsWith(suffix);
+      }
+
+      return userGroupSet.has(group);
+    });
+  }
+
+  _isViewingProject(projects, currentProject) {
+    return projects.find((project = "") => project.toLowerCase() === currentProject.toLowerCase());
+  }
 }
 
+/** Redux-connected version of _ChopsAnnouncement. */
+export class ChopsAnnouncement extends connectStore(_ChopsAnnouncement) {
+  /** @override */
+  stateChanged(state) {
+    const { displayName, groups } = userV0.currentUser(state);
+    this.currentUserName = displayName;
+    this.userGroups = groups;
+
+    this.currentProject = projectV0.viewedProjectName(state);
+  }
+}
+
+customElements.define('chops-announcement-base', _ChopsAnnouncement);
 customElements.define('chops-announcement', ChopsAnnouncement);