tofuness/Toshocat

View on GitHub
src/actions/list.js

Summary

Maintainability
A
0 mins
Test Coverage
import {
  LOAD_LIST,
  SORT_LIST,
  SEARCH_LIST,
  UPDATE_LIST_ITEM,
  ADD_LIST_ITEM,
  REMOVE_LIST_ITEM,
  FILTER_LIST_STATUS,
  SWITCH_LIST_FAILURE,
  UPDATE_CURRENT_LIST_NAME,
  SWITCH_SYNCER,
  SWITCH_SYNCER_FAILURE,
  SYNC_LIST,
  UPDATE_HEADER_ORDER
} from '../constants/actionTypes';
import request from 'superagent';
import moment from 'moment';
import _ from 'lodash';

import Syncer from '../syncers/Syncer';

import toshoStore from '../utils/store';
import settings from '../utils/settings';

/**
 * Loads list with currentListName into Store
 * @return {Function}
 */
export function loadList() {
  return (dispatch, getState) => {
    const { currentListName } = getState();
    dispatch({
      type: LOAD_LIST,
      currentList: toshoStore.getList(currentListName)
    });
  };
}

/**
 * Switch to another list
 * @param  {String} listName List name
 * @return {Object|Function}
 */
export function switchList(listName) {
  if (!listName) {
    return { type: SWITCH_LIST_FAILURE };
  }
  settings.set({ listName });
  return (dispatch) => {
    dispatch({
      type: UPDATE_CURRENT_LIST_NAME,
      currentListName: listName
    });
    dispatch(loadList());
  };
}

/**
 * Syncs new list with a list stored in local storage.
 * Makes sure that old fields are not naively removed.
 * @param  {String} listName List name
 * @param  {Array} newList   List
 * @return {Function}
 */
export function syncList(listName, newList) {
  return (dispatch) => {
    const unsyncedList = toshoStore.getList(listName);
    const syncedList = _.map(newList, (entry) => {
      // Check if we have this entry since before
      const unsyncedEntry = _.find(unsyncedList, (o) => {
        return o._id === entry._id;
      });

      if (unsyncedEntry) {
        return _.merge({}, entry, unsyncedEntry);
      }
      return entry;
    });

    toshoStore.saveList(listName, syncedList);

    dispatch({
      type: SYNC_LIST,
      syncedList
    });
  };
}

/**
 * Update the order of headers
 * @param  {Array} headers
 * @return {Object}
 */
export function updateHeaderOrder(headers) {
  const newOrder = _.sortBy(settings.get('headerOrder'), (header) => {
    return headers.indexOf(header.name);
  });
  settings.set('headerOrder', newOrder);
  return {
    type: UPDATE_HEADER_ORDER,
    order: newOrder
  };
}

/**
 * Switch to new syncer
 * @param  {Syncer} syncer Valid subclass of Syncer
 * @return {Object}        Action object
 */
export function switchSyncer(syncer) {
  if (!(syncer instanceof Syncer)) {
    return { type: SWITCH_SYNCER_FAILURE };
  }
  return {
    type: SWITCH_SYNCER,
    syncer
  };
}

/**
 * Remove current syncer if there is any
 * @return {Object}
 */
export function removeSyncer() {
  return {
    type: SWITCH_SYNCER,
    syncer: null
  };
}

/**
 * Sets status to filter list by
 * @param  {String} status E.g 'current'
 * @return {Object} Filter action
 */
export function filterListByStatus(status) {
  return {
    type: FILTER_LIST_STATUS,
    status
  };
}

/**
 * Search list and show match results only
 * @param  {String} query String that should match list entry titles
 * @return {Object}       Search action
 */
export function searchList(query) {
  return {
    type: SEARCH_LIST,
    query
  };
}

/**
 * Sort current visible list by a property
 * @param  {String} nextListSortBy List item property
 * @return {Function}
 */
export function sortListBy(nextListSortBy) {
  return (dispatch, getState) => {
    const { listSortBy } = getState();
    dispatch({
      type: SORT_LIST,
      prevListSortBy: listSortBy,
      listSortBy: nextListSortBy
    });
  };
}

/**
 * Add series to list
 * @param {Object} item List entry
 * @return {Function}
 */
export function addItem(item) {
  return (dispatch, getState) => {
    const { currentList, currentListName, currentSyncer } = getState();
    if (currentSyncer) {
      currentSyncer.addListItem(item);
    }
    const updatedList = [item, ...currentList];
    toshoStore.saveList(currentListName, updatedList);
    dispatch({
      type: ADD_LIST_ITEM,
      currentList: updatedList
    });
  };
}

/**
 * Updates list with new entry, replacing old one
 * @param  {Object} item new list entry
 * @return {Function}
 */
function _updateItem(item) {
  return (dispatch, getState) => {
    const { currentList, currentListName } = getState();
    const updatedList = currentList.map((listItem) => {
      return listItem._id === item._id ? _.merge({}, listItem, item) : listItem;
    });
    toshoStore.saveList(currentListName, updatedList);
    dispatch({
      type: UPDATE_LIST_ITEM,
      currentList: updatedList
    });
  };
}

/**
 * Fetches new information if series is currently airing
 * and updates the list with new entry.
 * @param  {Object} item new list entry
 * @return {Function}
 */
export function updateItem(item) {
  return (dispatch, getState) => {
    const { currentSyncer } = getState();
    if (currentSyncer) {
      currentSyncer.updateListItem(item);
    }
    // We always run a normal update to make sure the list entry
    // transitions correctly. New info can come in later after the user
    // has received a response to their action.
    dispatch(_updateItem(item));
    if (item.last_updated === undefined || moment().diff(item.last_updated, 'hours') > 24) {
      // E.g. if we are missing total episodes info etc
      return new Promise((resolve) => {
        request
        .get(`${settings.get('APIBase')}/anime/${item.mal_id}`)
        .end((err, res) => {
          if (!err && res.body) {
            dispatch(
              _updateItem(
                _.merge({}, item, res.body)
              )
            );
          }
          resolve();
        });
      });
    }
    return Promise.resolve();
  };
}

/**
 * Update list entry if exists; Add if it doesn't.
 * Use this if in doubt.
 * @param  {Object} item List entry
 * @return {Function}
 */
export function upsertItem(item) {
  return (dispatch, getState) => {
    const { currentList } = getState();
    const alreadyExists = _.find(currentList, (listItem) => {
      return listItem._id === item._id;
    });
    if (alreadyExists) {
      return dispatch(updateItem(item));
    }
    return dispatch(addItem(_.merge({}, {
      item: {
        item_status_text: 'Current',
        item_status: 'current',
        item_progress: 0,
        item_rating: 0,
        last_updated: new Date()
      }
    }, item)));
  };
}

/**
 * Increment or decrement (negative number) progress
 * @param  {Object} entry     List entry
 * @param  {Number} increment Positive or negative number
 * @return {Function}
 */
export function incrementProgress(entry, increment) {
  const { item } = entry;
  const total = entry.episodes_total || entry.chapters || 0;
  let incrementedProgress = Math.max(item.item_progress + (increment || 1), 0);
  if (total > 0) {
    incrementedProgress = Math.min(incrementedProgress, total);
  }
  return (dispatch) => {
    return dispatch(
      updateItem(
        _.merge({}, entry, {
          item: {
            item_status_text: incrementedProgress === total ? 'Completed' : item.item_status_text,
            item_status: incrementedProgress === total ? 'completed' : item.item_status,
            item_progress: incrementedProgress === total ? total : incrementedProgress,
            last_updated: entry.item.last_updated > new Date()
                          ? entry.item.last_updated
                          : new Date()
          }
        })
      )
    );
  };
}

/**
 * Removes list entries with matching _id
 * @param  {ObjectId} id ObjectId from MongoDB.
 * @return {Function}
 */
export function removeItem(id) {
  return (dispatch, getState) => {
    const { currentList, currentListName, currentSyncer } = getState();
    const updatedList = currentList.filter((listItem) => {
      if (id === listItem._id && currentSyncer) {
        currentSyncer.removeListItem(listItem);
      }
      return id !== listItem._id;
    });
    toshoStore.saveList(currentListName, updatedList);
    dispatch({
      type: REMOVE_LIST_ITEM,
      currentList: updatedList
    });
  };
}