Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/reducers/ui.js b/static_src/reducers/ui.js
new file mode 100644
index 0000000..871cf87
--- /dev/null
+++ b/static_src/reducers/ui.js
@@ -0,0 +1,170 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {combineReducers} from 'redux';
+import {createReducer} from './redux-helpers.js';
+
+/** @typedef {import('redux').AnyAction} AnyAction */
+
+const DEFAULT_SNACKBAR_TIMEOUT_MS = 10 * 1000;
+
+
+/**
+ * Object of various constant strings used to uniquely identify
+ * snackbar instances used in the app.
+ * TODO(https://crbug.com/monorail/7491): Avoid using this Object.
+ * @type {Object<string, string>}
+ */
+export const snackbarNames = Object.freeze({
+  // Issue list page snackbars.
+  FETCH_ISSUE_LIST_ERROR: 'FETCH_ISSUE_LIST_ERROR',
+  FETCH_ISSUE_LIST: 'FETCH_ISSUE_LIST',
+  UPDATE_HOTLISTS_SUCCESS: 'UPDATE_HOTLISTS_SUCCESS',
+
+  // Issue detail page snackbars.
+  ISSUE_COMMENT_ADDED: 'ISSUE_COMMENT_ADDED',
+});
+
+// Actions
+const INCREMENT_NAVIGATION_COUNT = 'INCREMENT_NAVIGATION_COUNT';
+const REPORT_DIRTY_FORM = 'REPORT_DIRTY_FORM';
+const CLEAR_DIRTY_FORMS = 'CLEAR_DIRTY_FORMS';
+const SET_FOCUS_ID = 'SET_FOCUS_ID';
+export const SHOW_SNACKBAR = 'SHOW_SNACKBAR';
+const HIDE_SNACKBAR = 'HIDE_SNACKBAR';
+
+/**
+ * @typedef {Object} Snackbar
+ * @param {string} id Unique string identifying the snackbar.
+ * @param {string} text The text to show in the snackbar.
+ */
+
+/* State Shape
+{
+  navigationCount: number,
+  dirtyForms: Array,
+  focusId: String,
+  snackbars: Array<Snackbar>,
+}
+*/
+
+// Reducers
+
+
+const navigationCountReducer = createReducer(0, {
+  [INCREMENT_NAVIGATION_COUNT]: (state) => state + 1,
+});
+
+/**
+ * Saves state on which forms have been edited, to warn the user
+ * about possible data loss when they navigate away from a page.
+ * @param {Array<string>} state Dirty form names.
+ * @param {AnyAction} action
+ * @param {string} action.name The name of the form being updated.
+ * @param {boolean} action.isDirty Whether the form is dirty or not dirty.
+ * @return {Array<string>}
+ */
+const dirtyFormsReducer = createReducer([], {
+  [REPORT_DIRTY_FORM]: (state, {name, isDirty}) => {
+    const newState = [...state];
+    const index = state.indexOf(name);
+    if (isDirty && index === -1) {
+      newState.push(name);
+    } else if (!isDirty && index !== -1) {
+      newState.splice(index, 1);
+    }
+    return newState;
+  },
+  [CLEAR_DIRTY_FORMS]: () => [],
+});
+
+const focusIdReducer = createReducer(null, {
+  [SET_FOCUS_ID]: (_state, action) => action.focusId,
+});
+
+/**
+ * Updates snackbar state.
+ * @param {Array<Snackbar>} state A snackbar-shaped slice of Redux state.
+ * @param {AnyAction} action
+ * @param {string} action.text The text to display in the snackbar.
+ * @param {string} action.id A unique global ID for the snackbar.
+ * @return {Array<Snackbar>} New snackbar state.
+ */
+export const snackbarsReducer = createReducer([], {
+  [SHOW_SNACKBAR]: (state, {text, id}) => {
+    return [...state, {text, id}];
+  },
+  [HIDE_SNACKBAR]: (state, {id}) => {
+    return state.filter((snackbar) => snackbar.id !== id);
+  },
+});
+
+export const reducer = combineReducers({
+  // Count of "page" navigations.
+  navigationCount: navigationCountReducer,
+  // Forms to be checked for user changes before leaving the page.
+  dirtyForms: dirtyFormsReducer,
+  // The ID of the element to be focused, as given by the hash part of the URL.
+  focusId: focusIdReducer,
+  // Array of snackbars to render on the page.
+  snackbars: snackbarsReducer,
+});
+
+// Selectors
+export const navigationCount = (state) => state.ui.navigationCount;
+export const dirtyForms = (state) => state.ui.dirtyForms;
+export const focusId = (state) => state.ui.focusId;
+
+/**
+ * Retrieves snackbar data from the Redux store.
+ * @param {any} state Redux state.
+ * @return {Array<Snackbar>} All the snackbars in the store.
+ */
+export const snackbars = (state) => state.ui.snackbars;
+
+// Action Creators
+export const incrementNavigationCount = () => {
+  return {type: INCREMENT_NAVIGATION_COUNT};
+};
+
+export const reportDirtyForm = (name, isDirty) => {
+  return {type: REPORT_DIRTY_FORM, name, isDirty};
+};
+
+export const clearDirtyForms = () => ({type: CLEAR_DIRTY_FORMS});
+
+export const setFocusId = (focusId) => {
+  return {type: SET_FOCUS_ID, focusId};
+};
+
+/**
+ * Displays a snackbar.
+ * @param {string} id Unique identifier for a given snackbar. We depend on
+ *   snackbar users to keep this unique.
+ * @param {string} text The text to be shown in the snackbar.
+ * @param {number} timeout An optional timeout in milliseconds for how
+ *   long to wait to dismiss a snackbar.
+ * @return {function(function): Promise<void>}
+ */
+export const showSnackbar = (id, text,
+    timeout = DEFAULT_SNACKBAR_TIMEOUT_MS) => (dispatch) => {
+  dispatch({type: SHOW_SNACKBAR, text, id});
+
+  if (timeout) {
+    window.setTimeout(() => dispatch(hideSnackbar(id)),
+        timeout);
+  }
+};
+
+/**
+ * Hides a snackbar.
+ * @param {string} id The unique name of the snackbar to be hidden.
+ * @return {any} A Redux action.
+ */
+export const hideSnackbar = (id) => {
+  return {
+    type: HIDE_SNACKBAR,
+    id,
+  };
+};