tahnik/devRantron

View on GitHub
app/src/js/actions/fetch.js

Summary

Maintainability
A
2 hrs
Test Coverage
import rantscript from '../consts/rantscript';
import { FEED, STATE, COLUMN, COLUMNS, ITEM } from '../consts/types';
import DEFAULT_STATES from '../consts/default_states';
import { getUID } from '../consts/utils';

const AMOUNT = 20;

let fetching = false;
let fetched = true;
let reload = null;

/**
 * devRant server sometimes returns duplicate for algo sorts. Duplicates
 * can happen for many other reasons as well. This function filters them.
 *
 * @param {array} orants Existing rants in a column
 * @param {array} newRants New rants fetched from devRant
 * @param {string} cFilters Custom filters set by user
 * @returns {array} filteredRants Filtered rants without duplicates
 */
const filterRants = (orants, newRants, cFilters) => {
  const ids = [];
  // Make an array of all the rants' id
  orants.map(rs => ids.push(rs.id));
  // Then using indexOf filter out the duplicates
  const rantsWithoutDuplicates = newRants.filter(rant => ids.indexOf(rant.id) === -1);
  // Check if the user has any custom filters, if there is then simply filter them
  if (cFilters) {
    /**
     * We have two type of filters. One is for rant body and another is for tags
     * first, split each one of those using comma as a separator and create an array
     */
    const content = cFilters.rant_content;
    const contentArray = content.split(',');
    for (let i = 0; i < contentArray.length; i += 1) {
      contentArray[i] = contentArray[i].trim();
    }
    const tags = cFilters.tags;
    const tagsArray = tags.split(',');
    for (let i = 0; i < tagsArray.length; i += 1) {
      tagsArray[i] = tagsArray[i].trim();
    }
    // Use those array to filter out the rants
    return rantsWithoutDuplicates.filter((rant) => {
      if (!(contentArray.length === 1 && contentArray[0] === '')) {
        for (let i = 0; i < contentArray.length; i += 1) {
          if (rant.text.toLowerCase().includes(contentArray[i])) {
            return false;
          }
        }
      }
      for (let i = 0; i < tagsArray.length; i += 1) {
        if (rant.tags.indexOf(tagsArray[i]) !== -1) {
          return false;
        }
      }
      return true;
    });
  }
  return rantsWithoutDuplicates;
};


/**
 * Returns the filters according to feed type i.e. sort (top, algo) and range (day, month)
 *
 * @param {string} type Type of the feed
 * @returns {object} filters Filters associated with the filter
 */
const getFilters = (type) => {
  switch (type) {
    case FEED.RANTS.NAME:
      return FEED.RANTS.FILTERS;
    case FEED.COLLABS.NAME:
      return FEED.COLLABS.FILTERS;
    case FEED.STORIES.NAME:
      return FEED.STORIES.FILTERS;
    default:
      return FEED.RANTS.FILTERS;
  }
};

/**
 * Only for collab this is necessary. The card that shows column is reusable
 * so it needs a flag to understand which contents to show
 *
 * @param {string} type Type of the feed
 * @returns {string} itemType Type of item the feed contains
 */
const getItemType = type => (type === FEED.COLLABS.NAME ? ITEM.COLLAB.NAME : ITEM.RANT.NAME);

/**
 * Adds a column in the custom component.
 *
 * @param {string} [type=FEED.RANTS.NAME] type of the feed that will be added
 */
const addColumn = (type = FEED.RANTS.NAME) => (dispatch) => {
  // get the default state of a column
  const column = DEFAULT_STATES.column;

  // Modify the column attributes as necessary
  const filters = getFilters(type);
  column.id = getUID();
  column.filters = filters;
  column.type = type;
  column.itemType = getItemType(type);

  dispatch({
    type: COLUMNS.ADD,
    column,
  });
};

/**
 * Removes a column in the custom component.
 *
 * @param {number} id ID of the column that will be removed
 */
const removeColumn = id => (dispatch) => {
  dispatch({
    type: COLUMNS.REMOVE,
    id,
  });
};

/**
 * Used to reset a column when switching between tabs or nav
 *
 */
const resetColumn = () => (dispatch) => {
  // Get a default state, this can be used to reset the column
  const column = DEFAULT_STATES.column;
  column.id = getUID();

  dispatch({
    type: COLUMN.RESET,
    column,
  });
};


/**
 * Updates the scroll height of a column. This is only used in custom columns
 *
 * @param {string} id
 * @param {number} value
 */
const updateColumnScrollHeight = (id, value) => (dispatch) => {
  dispatch({
    type: COLUMN.UPDATE_SCROLL,
    id,
    value,
  });
};


/**
 * fetches a feed.
 *
 * Why not have separate functions for them? Obviously reusablity. All the feeds
 * share a lot of similar codes.
 *
 * Why not separate function instead of switch cases? Eventually I realised I
 * was passing too many parameters. So switch case is actually less verbose here
 *
 * @param {string} sort Either algo, recent or top
 * @param {string} type The type of feed (rants, collabs)
 * @param {string} id ID of the specific column, used to identify a column in
 *                    array of columns
 * @param {string} range Either Day, Month, Year or All
 * @param {bool} refresh Indicates if the column should be refreshed from the
 *                       start
 */
const fetchFeed =
(sort, type, id, range, refresh = false, week = 0) => (dispatch, getState) => {
  // First check if column that requested the fetch is part of custom columns
  const columns = getState().columns;
  let currentColumn = columns.filter(column => column.id === id)[0];

  // If it isn't, then fetch was requested from single column feeds.
  if (!currentColumn) {
    if (getState().column.id === id) {
      currentColumn = getState().column;
    }
  }

  // Add this point we have the column that requested the fetch.

  /**
   * Check if there is an authenticated user, then get the auth token
   * which will be used while fetching the feed.
   * Using the token with the fetch returns which rants where upvoted or not
   */
  const { user } = getState().auth;
  let authToken = null;
  if (user) {
    authToken = user.authToken;
  }


  // Get the filters and item type associated with the feed
  const filters = getFilters(type);
  const itemType = getItemType(type);

  // Get the custom filters set by user
  let cFilters = { rant_content: '', tags: '' };
  const settings = getState().settings;
  if (settings && settings.general) {
    const generalSettings = settings.general;
    const filterRantOptions = generalSettings.filterRants.options;
    if (filterRantOptions.filter_enabled.value) {
      cFilters.rant_content = filterRantOptions.rant_content.value;
      cFilters.tags = filterRantOptions.tags.value;
    } else {
      cFilters = null;
    }
  }


  /**
   * Get the currently selected sort and range and compare them with new ones
   * If they have changed, make the page 0. This means we are doing a semi reset
   * Also, resets the pages if fetch requested a refresh
   */
  let page = 0;
  let prevSet = 0;
  prevSet = currentColumn.prev_set;
  const oldSort = currentColumn.sort;
  const oldRange = currentColumn.range;
  page = oldSort !== sort
         || oldRange !== range
         || refresh ? 0 : currentColumn.page;

  /**
   * If the pages is 0, that means we need to remove the existing items in the
   * column.
   * If not, just update the state to loading, that will be used by column
   * to make sure it is not requesting any fetch while there is a pending
   * request.
   */
  if (page === 0) {
    dispatch({
      type: COLUMN.FETCH,
      column: {
        ...currentColumn, items: [], page: 0, state: STATE.LOADING,
      },
    });
  } else {
    dispatch({
      type: COLUMN.FETCH,
      column: { ...currentColumn, state: STATE.LOADING },
    });
  }

  // Setup the new column. Reusability
  const newColumn = {
    id: currentColumn.id,
    sort,
    range,
    type,
    page: page === 0 ? 1 : currentColumn.page + 1,
    state: STATE.SUCCESS,
    filters,
    itemType,
  };

  // Switch between different feed types and fetches the right one.
  switch (type) {
    case FEED.RANTS.NAME: {
      rantscript
        .rants(sort, AMOUNT, AMOUNT * page, prevSet, authToken, range)
        .then((res) => {
          fetching = false;
          fetched = true;
          /**
             * If the pages is 0, that means we do not need to current items in the
             * column.
             */
          window.clearInterval(reload);
          const currentItems = page !== 0 ? currentColumn.items : [];
          newColumn.items = [
            ...currentItems,
            ...filterRants(currentItems, res.rants, cFilters),
          ];
          // The prev_set is needed for algo sort to work.
          newColumn.prev_set = res.set;
          dispatch({
            type: COLUMN.FETCH,
            column: newColumn,
          });
        })
        .catch(() => {
          fetching = false;
          //
        });
      break;
    }
    case FEED.STORIES.NAME: {
      rantscript
        .stories(range, sort, AMOUNT, AMOUNT * page, authToken)
        .then((res) => {
          fetching = false;
          fetched = true;
          window.clearInterval(reload);
          const currentItems = page !== 0 ? currentColumn.items : [];
          newColumn.items = [
            ...currentItems,
            ...filterRants(currentItems, res, cFilters),
          ];
          newColumn.prev_set = res.set;
          dispatch({
            type: COLUMN.FETCH,
            column: newColumn,
          });
        })
        .catch(() => {
          fetching = false;
          //
        });
      break;
    }
    case FEED.COLLABS.NAME: {
      rantscript
        .collabs(sort, AMOUNT, AMOUNT * page, authToken)
        .then((res) => {
          fetching = false;
          fetched = true;
          window.clearInterval(reload);
          const currentItems = page !== 0 ? currentColumn.items : [];
          newColumn.items = [
            ...currentItems,
            ...filterRants(currentItems, res, cFilters),
          ];
          newColumn.prev_set = res.set;
          dispatch({
            type: COLUMN.FETCH,
            column: newColumn,
          });
        })
        .catch(() => {
          fetching = false;
          //
        });
      break;
    }
    case FEED.WEEKLY.NAME: {
      rantscript
        .weekly(week, sort, AMOUNT, AMOUNT * page, authToken)
        .then((res) => {
          fetching = false;
          fetched = true;
          window.clearInterval(reload);
          const currentItems = page !== 0 ? currentColumn.items : [];
          newColumn.items = [
            ...currentItems,
            ...filterRants(currentItems, res, cFilters),
          ];
          newColumn.week = week;
          newColumn.prev_set = res.set;
          dispatch({
            type: COLUMN.FETCH,
            column: newColumn,
          });
        })
        .catch(() => {
          fetching = false;
          //
        });
      break;
    }
    default:
      dispatch();
  }
};

const fetch =
(sort, type, id, range, refresh = false, week = 0) => (dispatch) => {
  fetching = true;
  fetched = false;
  dispatch(fetchFeed(sort, type, id, range, refresh, week));
  reload = setInterval(() => {
    if (!fetching && !fetched) {
      dispatch(fetchFeed(sort, type, id, range, refresh, week));
    }
  }, 1000);
};

export {
  fetch as default,
  addColumn, resetColumn, removeColumn,
  updateColumnScrollHeight,
};