Source: app/selectors/collectionPaginationViewSelector.js

/* eslint-disable import/prefer-default-export */
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import { createSelector } from 'reselect';
import { COMMUNITY_GROUP } from '../data/entityTypes';
import { COMMUNITY_GROUP_POSTS } from '../data/collectionPaginationViews';
import { makeCollectionSelector } from './collectionSelector';
import { PAGINATION_OFFSET, PAGINATION_PARTITION } from '../data/paginationType';

/* eslint-disable max-len */
/**
 * Contains the function
 * {@link module:collectionPaginationViewSelector~makeCollectionPaginationViewSelector|makeCollectionPaginationViewSelector}
 * that can be used to select all the entities that should be visible according to the current
 * view pagination in the `collectionPaginationViewsReducer`
 * @module collectionPaginationViewSelector
 * @category redux-state
 */

/**
 * Result returned by a `collectionPaginationViewSelector`
 * @typedef CollectionPaginationViewSelectorResult
 * @property entities {Object[]} An array of data for the entities that are currently in view, if
 * found in the `entitiesSelector`
 * @property entityRefs {Object[]} An array of the entities that are currently in view
 * @property entityRefs[].id {string} The id of the entity
 * @property entityRefs[].type {string} The type of the entity
 * @property entityRefs[].data {*} The entity data, if found in the `entitiesSelector`. Note that
 * this is a reference to the same object as the one in the `entities` array, but it is also
 * included here for convenience
 * @property allEntityRefs {Object[]} An array of all entities in the collection
 * @property allEntityRefs[].id {string} The id of the entity
 * @property allEntityRefs[].type {string} The type of the entity
 * @property allEntityRefs[].data {*} The entity data, if found in the `entitiesSelector`
 * @property pagination {Object} An object with view pagination info
 */

/**
 * Selector returned by
 * {@link module:collectionPaginationViewSelector~makeCollectionPaginationViewSelector|makeCollectionPaginationViewSelector}
 * @param state {Object} The current redux state
 * @callback collectionPaginationViewSelector
 * @returns {CollectionPaginationViewSelectorResult} An object containing the selector data. See
 * {@link module:collectionPaginationViewSelector~CollectionPaginationViewSelectorResult|CollectionPaginationViewSelectorResult}
 */

/**
 * Returns a new {@link https://github.com/reactjs/reselect|reselect} selector that can be used
 * to get all the entities that should be visible according to the current
 * view pagination in the `collectionPaginationViewsReducer`.
 * @function makeCollectionPaginationViewSelector
 * @param collectionPaginationViewId {string} The id for the collectionPaginationView to read from
 * @returns {collectionPaginationViewSelector} The selector. See
 * {@link module:collectionPaginationViewSelector~collectionPaginationViewSelector|collectionPaginationViewSelector}
 * @example // CommunityOverview/index.js
 * const connector = connect(
 *   () => {
 *     const collectionPaginationViewSelector = makeCollectionPaginationViewSelector(
 *       POST_OVERVIEW
 *     );
 *
 *     return (state) => {
 *       const posts = collectionPaginationViewSelector(state);
 *
 *       return {
 *         posts: posts.entities,
 *         pagination: posts.pagination,
 *       };
 *     };
 *   },
 * );
 */
/* eslint-enable max-len */
export const makeCollectionPaginationViewSelector = collectionPaginationViewId => {
  const collectionSelector = makeCollectionSelector();

  return createSelector(
    state =>
      collectionSelector(state, {
        collectionId:
          (state.view.collectionPagination[collectionPaginationViewId] &&
            state.view.collectionPagination[collectionPaginationViewId].collection) ||
          null,
      }),
    state =>
      (state.view.collectionPagination[collectionPaginationViewId] &&
        state.view.collectionPagination[collectionPaginationViewId].offset) ||
      0,
    state =>
      (state.view.collectionPagination[collectionPaginationViewId] &&
        state.view.collectionPagination[collectionPaginationViewId].limit) ||
      0,
    ({ pagination: dataPagination, entityRefs: allEntityRefs }, offset, limit) => {
      const entityRefs = new Array(limit).fill(null);
      const { total, hasMore } = dataPagination;
      const pagination = { total, offset, limit, hasMore };
      const dataOffset = dataPagination.type === PAGINATION_OFFSET ? dataPagination.offset || 0 : 0;

      for (
        let i = Math.max(offset, dataOffset);
        i - dataOffset < allEntityRefs.length && i - offset < limit;
        i++
      ) {
        entityRefs[i - offset] = allEntityRefs[i - dataOffset];
      }

      if (dataPagination.type === PAGINATION_PARTITION) {
        pagination.before = { count: offset, atBegin: !offset && dataPagination.atBegin };
        const afterCount = Math.max(allEntityRefs.length - (limit + offset), 0);
        pagination.after = {
          count: afterCount,
          atEnd: !afterCount && dataPagination.atEnd,
        };
      } else {
        pagination.before = { count: offset, atBegin: !offset };
        if (total !== null) {
          const afterCount = total - (offset + limit);
          pagination.after = { count: afterCount, atEnd: afterCount <= 0 };
        }
      }

      return {
        entityRefs,
        entities: entityRefs.map(e => (e ? e.data : e)),
        allEntityRefs,
        pagination,
      };
    },
  );
};

export const postsCollectionPaginationViewSelector = (state, collectionPaginationViewId) => {
  const collectionPaginationViewSelector = makeCollectionPaginationViewSelector(
    collectionPaginationViewId,
  );

  const paginationData = collectionPaginationViewSelector(state);
  const communityGroups = state.entities[COMMUNITY_GROUP];

  let sortType = 'isPinned';

  // We have 2 seperate flags for isPinned due to the pinning logic, if a post is community group pinned
  // then it will not be pinned in the main community feed but will be pinned on the group page. This
  // couldn't be driven from one property due to the posts being stored in Redux under one entity
  // type which would overwrite the properties depending on where you fetched them. Storing them
  // as seperate entities would require rewriting the interactions logic.
  if (collectionPaginationViewId === COMMUNITY_GROUP_POSTS) {
    sortType = 'isCommunityGroupPinned';
  }

  const { entityRefs } = paginationData;
  const orderedItems = orderBy(entityRefs, post => (post ? post.data[sortType] : post), ['desc']);
  const uniqueItems = uniqBy(orderedItems, 'id');

  let posts = uniqueItems.filter(post => !!post) || [];

  posts = posts.map(({ data: post }) => ({
    ...post,
    communityGroup: communityGroups?.[post?.communityGroupId] || null,
  }));

  return {
    ...paginationData,
    entities: posts,
  };
};