chromium/ash/webui/common/resources/sea_pen/sea_pen_reducer.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';

import {SeaPenImageId} from './constants.js';
import {MantaStatusCode, RecentSeaPenThumbnailData, SeaPenQuery, SeaPenThumbnail, TextQueryHistoryEntry} from './sea_pen.mojom-webui.js';
import {SeaPenActionName, SeaPenActions} from './sea_pen_actions.js';
import {SeaPenLoadingState, SeaPenState} from './sea_pen_state.js';

function loadingReducer(
    state: SeaPenLoadingState, action: SeaPenActions): SeaPenLoadingState {
  switch (action.name) {
    case SeaPenActionName.BEGIN_SEARCH_SEA_PEN_THUMBNAILS:
      return {
        ...state,
        thumbnails: true,
      };
    case SeaPenActionName.CLEAR_SEA_PEN_THUMBNAILS_LOADING:
    case SeaPenActionName.SET_SEA_PEN_THUMBNAILS:
      return {
        ...state,
        thumbnails: false,
      };
    case SeaPenActionName.BEGIN_LOAD_RECENT_SEA_PEN_IMAGES:
      return {
        ...state,
        recentImages: true,
      };
    case SeaPenActionName.SET_RECENT_SEA_PEN_IMAGES:
      const newRecentImages: SeaPenImageId[] =
          Array.isArray(action.recentImages) ? action.recentImages : [];
      // Only keep loading state for most recent Sea Pen images.
      return {
        ...state,
        recentImageData: newRecentImages.reduce(
            (result, next) => {
              if (state.recentImageData.hasOwnProperty(next)) {
                result[next] = state.recentImageData[next];
              }
              return result;
            },
            {} as Record<SeaPenImageId, boolean>),
        // Recent image list is done loading.
        recentImages: false,
      };
    case SeaPenActionName.BEGIN_LOAD_RECENT_SEA_PEN_IMAGE_DATA:
      return {
        ...state,
        recentImageData: {
          ...state.recentImageData,
          [action.id]: true,
        },
      };
    case SeaPenActionName.SET_RECENT_SEA_PEN_IMAGE_DATA:
      return {
        ...state,
        recentImageData: {
          ...state.recentImageData,
          [action.id]: false,
        },
      };
    case SeaPenActionName.BEGIN_SELECT_RECENT_SEA_PEN_IMAGE:
      return {...state, setImage: state.setImage + 1};
    case SeaPenActionName.END_SELECT_RECENT_SEA_PEN_IMAGE:
    case SeaPenActionName.END_SELECT_SEA_PEN_THUMBNAIL:
      if (state.setImage <= 0) {
        console.error('Impossible state for loading.setImage');
        // Reset to 0.
        return {...state, setImage: 0};
      }
      return {...state, setImage: state.setImage - 1};
    case SeaPenActionName.BEGIN_LOAD_SELECTED_RECENT_SEA_PEN_IMAGE:
      return {
        ...state,
        currentSelected: true,
      };
    case SeaPenActionName.BEGIN_SELECT_SEA_PEN_THUMBNAIL:
      return {
        ...state,
        currentSelected: true,
        setImage: state.setImage + 1,
      };
    case SeaPenActionName.SET_SELECTED_RECENT_SEA_PEN_IMAGE:
      return {
        ...state,
        currentSelected: false,
      };
    default:
      return state;
  }
}

function thumbnailResponseStatusCodeReducer(
    state: MantaStatusCode|null, action: SeaPenActions): MantaStatusCode|null {
  switch (action.name) {
    case SeaPenActionName.SET_THUMBNAIL_RESPONSE_STATUS_CODE:
      return action.thumbnailResponseStatusCode;
    default:
      return state;
  }
}

function currentSelectedReducer(
    state: SeaPenImageId|null, action: SeaPenActions): SeaPenImageId|null {
  switch (action.name) {
    case SeaPenActionName.SET_SELECTED_RECENT_SEA_PEN_IMAGE:
      return action.key;
    default:
      return state;
  }
}

/**
 * Reducer for the pending selected image. The pendingSelected state is set when
 * a user clicks on an image and before the client code is reached.
 *
 * Note: We allow multiple concurrent requests of selecting images while only
 * keeping the latest pending image and failing others occurred in between.
 * The pendingSelected state should not be cleared in this scenario (of multiple
 * concurrent requests). Otherwise, it results in a unwanted jumpy motion of
 * selected state.
 */
function pendingSelectedReducer(
    state: SeaPenImageId|SeaPenThumbnail|null, action: SeaPenActions,
    globalState: SeaPenState): SeaPenImageId|SeaPenThumbnail|null {
  switch (action.name) {
    case SeaPenActionName.BEGIN_SELECT_RECENT_SEA_PEN_IMAGE:
      return action.id;
    case SeaPenActionName.SET_SELECTED_RECENT_SEA_PEN_IMAGE:
      const {key} = action;
      if (state && !key) {
        console.warn('pendingSelectedReducer: Failed to get selected image.');
        return null;
      } else if (globalState.loading.setImage == 0) {
        // Clear the pending state when there are no more requests.
        return null;
      }
      return state;
    case SeaPenActionName.END_SELECT_RECENT_SEA_PEN_IMAGE:
    case SeaPenActionName.END_SELECT_SEA_PEN_THUMBNAIL:
      const {success} = action;
      if (!success && globalState.loading.setImage <= 1) {
        // Clear the pending selected state if an error occurs and
        // there are no multiple concurrent requests of selecting images.
        return null;
      }
      return state;
    case SeaPenActionName.BEGIN_SELECT_SEA_PEN_THUMBNAIL:
      return action.thumbnail;
    default:
      return state;
  }
}

function recentImagesReducer(
    state: SeaPenImageId[]|null, action: SeaPenActions): SeaPenImageId[]|null {
  switch (action.name) {
    case SeaPenActionName.SET_RECENT_SEA_PEN_IMAGES:
      return action.recentImages;
    default:
      return state;
  }
}

function recentImageDataReducer(
    state: Record<SeaPenImageId, RecentSeaPenThumbnailData|null>,
    action: SeaPenActions):
    Record<SeaPenImageId, RecentSeaPenThumbnailData|null> {
  switch (action.name) {
    case SeaPenActionName.SET_RECENT_SEA_PEN_IMAGES:
      const newRecentImages: SeaPenImageId[] =
          Array.isArray(action.recentImages) ? action.recentImages : [];
      return newRecentImages.reduce((result, id) => {
        if (id && state.hasOwnProperty(id)) {
          result[id] = state[id];
        }
        return result;
      }, {} as typeof state);
    case SeaPenActionName.SET_RECENT_SEA_PEN_IMAGE_DATA:
      return {...state, [action.id]: action.data};
    default:
      return state;
  }
}

function currentSeaPenQueryReducer(
    state: SeaPenQuery|null, action: SeaPenActions): SeaPenQuery|null {
  switch (action.name) {
    case SeaPenActionName.SET_CURRENT_SEA_PEN_QUERY:
      assert(!!action.query, 'query is empty.');
      return action.query;
    case SeaPenActionName.CLEAR_CURRENT_SEA_PEN_QUERY:
      return null;
    default:
      return state;
  }
}

function thumbnailsReducer(
    state: SeaPenThumbnail[]|null, action: SeaPenActions): SeaPenThumbnail[]|
    null {
  switch (action.name) {
    case SeaPenActionName.SET_SEA_PEN_THUMBNAILS:
      assert(!!action.query, 'input text is empty.');
      return action.thumbnails;
    case SeaPenActionName.CLEAR_SEA_PEN_THUMBNAILS:
      return null;
    default:
      return state;
  }
}

function shouldShowSeaPenIntroductionDialogReducer(
    state: boolean, action: SeaPenActions): boolean {
  switch (action.name) {
    case SeaPenActionName.SET_SHOULD_SHOW_SEA_PEN_INTRODUCTION_DIALOG:
      return action.shouldShowDialog;
    default:
      return state;
  }
}

function errorReducer(state: string|null, action: SeaPenActions): string|null {
  switch (action.name) {
    case SeaPenActionName.END_SELECT_RECENT_SEA_PEN_IMAGE:
    case SeaPenActionName.END_SELECT_SEA_PEN_THUMBNAIL:
      if (!action.success) {
        // TODO(b/332743948) make error messages for this flow use manta status
        // code.
        return loadTimeData.getString('seaPenErrorGeneric');
      }
      return null;
    case SeaPenActionName.BEGIN_SELECT_SEA_PEN_THUMBNAIL:
    case SeaPenActionName.BEGIN_SELECT_RECENT_SEA_PEN_IMAGE:
    case SeaPenActionName.DISMISS_SEA_PEN_ERROR_ACTION:
    case SeaPenActionName.BEGIN_SEARCH_SEA_PEN_THUMBNAILS:
    case SeaPenActionName.CLEAR_SEA_PEN_THUMBNAILS:
      return null;
    default:
      return state;
  }
}

function textQueryHistoryReducer(
    state: TextQueryHistoryEntry[]|null,
    action: SeaPenActions): TextQueryHistoryEntry[]|null {
  switch (action.name) {
    case SeaPenActionName.SET_SEA_PEN_TEXT_QUERY_HISTORY:
      return action.history;
    default:
      return state;
  }
}

export function seaPenReducer(
    state: SeaPenState, action: SeaPenActions): SeaPenState {
  const newState = {
    loading: loadingReducer(state.loading, action),
    recentImageData: recentImageDataReducer(state.recentImageData, action),
    recentImages: recentImagesReducer(state.recentImages, action),
    thumbnailResponseStatusCode: thumbnailResponseStatusCodeReducer(
        state.thumbnailResponseStatusCode, action),
    thumbnails: thumbnailsReducer(state.thumbnails, action),
    currentSeaPenQuery:
        currentSeaPenQueryReducer(state.currentSeaPenQuery, action),
    currentSelected: currentSelectedReducer(state.currentSelected, action),
    pendingSelected:
        pendingSelectedReducer(state.pendingSelected, action, state),
    shouldShowSeaPenIntroductionDialog:
        shouldShowSeaPenIntroductionDialogReducer(
            state.shouldShowSeaPenIntroductionDialog, action),
    error: errorReducer(state.error, action),
    textQueryHistory: textQueryHistoryReducer(state.textQueryHistory, action),
  };
  return newState;
}