Source

externalCode/redux.js

import {Observable} from "rxjs";
//$FlowFixMe
import {Store} from "redux";

/**
 * Redux reducers implemented in app
 * @typedef {"accountSettings" | "documentsCache" | "documents" | "activities" | "activitiesCache" | "albums" | "appFeedback" | "auth" | "blockData" | "blockUser" | "blockLoadableDataCache" | "blockLoadableDataError" | "blockLoadableDataLoading" | "blog" | "blogCache" | "blogSingleComments" | "blogSingleCommentsCache" | "config" | "courses" | "courseCategories" | "coursesCache" | "coursesByCategory" | "emailInvites" | "featureStatuses" | "firebase" | "fonts" | "forgot" | "forums" | "forumsCache" | "friendRequests" | "gifs" | "groupDocuments" | "groupActivities" | "groupMembers" | "groups" | "groupsCache" | "groupInvites" | "groupMessages" | "groupRequests" | "groupCourses" | "groupSettings" | "localAuthentication" | "media" | "files" | "preview" | "members" | "messages" | "messagesCache" | "thread" | "menuSettings" | "merge" | "move" | "notifications" | "notificationsCache" | "privacySettings" | "profileDocuments" | "profileActivities" | "profileBlog" | "profileCourses" | "groupAlbums" | "groupPhotos" | "globalPhotos" | "profilePhotos" | "hostedPhotos" | "photos" | "mediaCache" | "profileAlbums" | "albumsCache" | "profileFriends" | "profileGroups" | "profileTopics" | "profileTabs" | "tagTopics" | "profileReplies" | "repliesCache" | "reportContentCategories" | "reportContentRequest" | "settings" | "signup" | "signupFields" | "singleAlbumPhotos" | "singleCourse" | "singleGroup" | "singleForum" | "singleLearnTopic" | "singleLesson" | "singleQuiz" | "singleQuizQuestions" | "singleTopic" | "sites" | "socialGroups" | "split" | "survey" | "subgroups" | "cardModal" | "groupTabs" | "topicCache" | "topicDropdown" | "topics" | "trackPlayer" | "urls" | "user" | "userProfile" | "usersCache" | "xProfile" | "inAppPurchases" | "appInitialisation" | "profileDetails" | "notificationSubscriptions" | "network" } AppReducers
 */

/**
 * Redux reducers implemented in app
 * @typedef {"loadEmailPreferences" | "updateEmailPreferences" | "getEmailInvites" | "revokeEmailInvite" | "checkBiometricSupport" | "checkBiometricEnrollmentStatus" | "sendAppFeedback" | "fetchPhotoActivity" | "createNewPhotos" | "deletePhotos" | "updatePhoto" | "albumsTree" | "loadHostedPhotos" | "loadAlbumPhotos" | "editAlbum" | "loadProfileBlog" | "fetchPost" | "loadBlog" | "loadBlogSingleComments" | "loadBlogCategories" | "loadBlogCreationTimes" | "deleteBlogComment" | "blogComment" | "loadGroupTypes" | "loadProfileTypes" | "viewAsRestore" | "viewAsSwitch" | "deleteAccount" | "loadAccountSettings" | "loadPrivacySettings" | "updatePrivacySettings" | "loadLoginInfo" | "updateLoginInfo" | "loadGroupInviteSettings" | "updateGroupInviteSettings" | "fetchActivity" | "updateActivity" | "updateViewAsSession" | "restNoRouteHandler" | "removeFriend" | "loadFriendRequests" | "friendRequestCreate" | "friendRequestModify" | "loadGroupInvites" | "groupInviteModify" | "groupInviteCreate" | "loadGroupRequests" | "groupRequestCreate" | "loadProfileTopics" | "loadTagTopics" | "groupRequestModify" | "loadSignupFields" | "signup" | "socialLogin" | "forgot" | "getUser" | "getAnyUser" | "navigateToProfile" | "loadProfileReplies" | "navigateToGroup" | "loadUserProfileData" | "loadTopicForUser" | "loadSubGroups" | "loadGroups" | "joinGroup" | "leaveGroup" | "groupsDetails" | "loadSocialGroups" | "joinSocialGroup" | "loadSingleSocialGroup" | "loadGroupSettings" | "socialGroupTabRequest" | "notificationAction" | "loadNotifications" | "markNotification" | "deleteNotification" | "loadForums" | "subscribeForum" | "unsubscribeForum" | "navigateToSubforum" | "loadTopics" | "fetchTopic" | "subscribeTopic" | "unsubscribeTopic" | "newTopicEpic" | "closeKeyBoard" | "blockUser" | "unblockUser" | "loadSingleForum" | "loadForumFromSubForum" | "loadRepliesForTopic" | "unfavoriteTopic" | "favoriteTopic" | "openTopic" | "closeTopic" | "stickTopic" | "unstickTopic" | "superstickTopic" | "spamTopic" | "unspamTopic" | "trashTopic" | "loadRelatedTopicsForTopic" | "spamReply" | "unspamReply" | "trashReply" | "newReply" | "refreshRepliesForTopic" | "loadDropdownTopics" | "doMergeEpic" | "doSplitEpic" | "doMoveEpic" | "uploadMedia" | "uploadDocument" | "getPreviewData" | "loadMenuSettings" | "loadSites" | "loadCoursesOnRequest" | "loadCoursesByCategoryOnRequest" | "loadCoursesWithCategoriesOnRequest" | "loadSingleCourse" | "loadSingleCourseMembers" | "loadSingleTopic" | "completeTopic" | "coursesDetails" | "enrollCourse" | "loadSingleLessonWithTopics" | "completeLesson" | "loadSingleQuiz" | "startQuizEpic" | "completeQuiz" | "updateSettings" | "loadReportCategories" | "reportContentRequest" | "clearCookiesOnLogout" | "loadFriendsToList" | "loadGroupDocumentsToList" | "loadProfileDocumentsToList" | "loadProfileActivitiesToList" | "loadProfileGroups" | "loadGroupMembersToList" | "loadGroupActivitiesToList" | "loadMembersToList" | "membersDetails" | "loadMembersAdvancedSearchForm" | "loadActivitiesToList" | "loadMessagesToList" | "loadThread" | "deleteThread" | "actionThread" | "newMessage" | "favoriteActivity" | "pinActivity" | "replyToActivity" | "deleteActivity" | "newActivity" | "documentsDetails" | "foldersTree" | "fetchFolder" | "loadDocumentsToList" | "deleteDocument" | "newDocument" | "updateDocument" | "activitiesDetails" | "followMember" | "resetStateOnLogout" | "blockDataRequest" | "loadBlockContent" | "memberProfileFields" | "memberProfileFieldsEdit" | "uploadProfileImage" | "deleteUserPhoto" | "uploadGroupImage" | "uploadGroupCover" | "groupUpdate" | "deleteGroup" | "deleteGroupCover" | "deleteGroupPhoto" | "promoteMember" | "bulkResources" | "surveyEntriesRequest" | "surveyFormDataRequest" | "surveyResultsRequest" | "loadInAppProducts" | "makePurchase" | "registerPurchaseListeners" | "loadProductsForScreen" | "applyLoadedData" | "loadDataForAppInitialisation" | "loadUsersToCache" | "askFileUploadPermissions" | "navigateToTopic" | "navigateToAddTopic" | "loadNotificationSubscriptions" | "updateNotificationSubscriptions" | "initializeFirebase" | "manageNotifications" | "manageFirePushToken" | "updateFirebaseTopics" | "setNativeBadgeValue" | "notificationClickHandle" | "loadProfileCourses" | "loadProfilePhotos" | "selectSinglePhoto" | "deleteProfilePhoto" | "uploadProfilePhoto" | "fetchProfileAlbums" | "createProfileAlbum" | "setFilterPhoto" | "uploadProfileAlbumPhotoRequest" | "deleteProfileAlbumPhoto" | "deleteProfileAlbum" | "loadPhotoActivity" | "replyToPhotoComment" | "loadPhotoActivityItem" | "photoFavoriteActivity" | "addPhotoComment" | "deletePhotoActivity" | "loadGroupPhotos" | "deleteGroupPhoto" | "uploadGroupPhoto" | "setGroupFilterPhoto" | "fetchGroupAlbums" | "uploadProfileAlbumPhotoRequest" | "deleteGroupAlbumPhoto" | "createGroupAlbum" | "deleteGroupAlbum" | "loadGroupCourses" | "loadGlobalPhotosToList" | "loadGlobalPhotosDetails" | "validateToken" | "loadProfileTabs" | "loadGifs" | "completePurchase" | "addRepeater" | "deleteRepeater" | "reorderRepeaters"} AppEpic
 */

/**
 * @typedef {Function} ValidationFunction
 * @param  { Object } responseObject Object returned in response
 * @return {boolean}
 */

/**
 * @typedef {Object} CancelablePromise
 * @param {Function} cancel
 * @param {Promise} promise
 * @param {Promise} then then function of 'promise'
 * @param {Promise} catch catch function of 'promise'
 */

/**
 * @typedef {Function} CustomAPIRequestFunction
 * @param  { string } urlPath URL
 * @param  { "post" | "get" | "delete" | "patch" } method
 * @param  { Object } paramsOrPayload Params to send with request body
 * @param  { ValidationFunction } validation Runs a validation over response object. You can define your own function or use `objectValidation` (checks if response is an object) or `arrayOfObjectsValidation` (checks if response is an array of objects). If validation fails, app will go in offline mode.
 * @param  { Object } headers Define request headers
 * @param  { boolean } isfullUrl Set `true` if you want to use the full URL, instead of appending the path to your connected site URL.
 * @returns {CancelablePromise}
 */

/**
 * @typedef {Function} TranslationFunction
 * @param {string}
 * @return {string}
 * @example
 * t("common:ok") // gives "Ok"
 */

/**
  * @typedef {Object} RequestsAPI
  * @property {CustomAPIRequestFunction} customRequest Used to make custom API requests
 
 /**
  * @typedef {Function} GetAPI
  * @param {Object} - App configuration object that you can get from redux store.config.
  * @return {RequestsAPI} - Object containing many methods for fetching specific data as well as general method `customRequest` for fetching from any REST API url.
  */

/**
 * @typedef {Object} IOMiddlewareOptions
 * @property {NavigationService} navigationService Object similar to the navigation property of "@react-navigation/native". The most used methods are `navigate` and`dispatc`. Both are used for triggering navigation.
 * @property {BuildConfigType} buildConfig Build configuration object which contains all things required for specific app builds, including URL to the app icon, app launch screen, and many more.
 * @property {Object} inApp "react-native-iap" default export object
 * @property {GetAPI} getApi Function that gets an instance of the class containing all the methods used for fetching data
 * @property {TranslationFunction} t
 */

/**
 * Redux action
 * @typedef {Object} ReduxAction
 * @property {String} type Indicator for the handling of the action
 * @see {@link https://redux.js.org/basics/actions}
 */

/**
 * Redux reducer
 * @typedef {Function} Reducer
 * @param {ReduxStore} previousState The current state
 * @param {ReduxAction} action An action to execute to update the store
 * @return {ReduxStore} The next state
 * @see {@link https://redux.js.org/basics/reducers}
 */

/**
 * @typedef ReduxStore
 * @type {Object}
 * @see {@link https://redux.js.org/basics/store}
 */

/**
 * @typedef {Function} ReduxMiddleware
 * @see {@link https://redux.js.org/understanding/history-and-design/middleware}
 */

/**
 * @typedef {Function} ReducerWrapper
 * @param {Reducer} - Original reducer
 * @return {Reducer} - Changed reducer
 */

/**
 * @typedef {Object} StoreApi
 * @param {Function} getState Returns redux store object
 * @param {Function} dispatch Dispatches the action
 */

/**
 * @typedef {Object} Epic
 * @param {Observable<ReduxAction>} observable
 * @param {StoreApi} storeApi
 * @param {IOMiddlewareOptions} dependencies
 * @return {Observable<ReduxAction>}
 * @see {@link https://redux-observable.js.org/docs/basics/Epics.html}
 */

/**
 * @typedef {Function} EpicWrapper
 * @param {Epic} - Original epic
 * @return {Epic} - Changed epic
 */

/**
 * @class
 * Redux Hooks.
 * Instance name: reduxApi
  
   You can use these hooks to customize the redux related configurations of your app such as adding a new epic, new middleware to store initialization, and more.
 * @example
 * externalCodeSetup.reduxApi.METHOD_NAME
 */
export class ReduxApi {
	newReducers = {};
	reducerWrappers = {};
	newEpics = {};
	epicWrappers = {};
	storeCreateListeners = [];
	newMiddlewares = [];

	persitorConfigChangers = [];
	customPersistorConfig = undefined;

	isDataPersistingEnabled = true;

	/**
	 * Adds a new reducer to the root reducer.
	 * Note: If "name" used to add a reducer already exists, the new reducer will replace the old reducer.
	 * @method
	 * @param {String} name Name of reducer
	 * @param {Reducer} reducer Reducer function
	 * @example <caption> Create a new reducer </caption>
	 *
	 *  const initialState = {
	 *    loading: false,
	 *    loaded: false,
	 *    errorMessage: null,
	 *    foodReceived: ""
	 *  }
	 *  const delivery = (state = initialState, action) => {
	 *    switch (action.type) {
	 *      case "FOOD_DELIVERY_REQUEST": {
	 *        return {
	 *          ...state,
	 *          loading: true,
	 *          loaded: false,
	 *          errorMessage: null
	 *        };
	 *      }
	 *      case "FOOD_DELIVERY_SUCCESS": {
	 *        return {
	 *          ...state,
	 *          foodReceived: action.foodToDeliver,
	 *          loading: false,
	 *          loaded: true,
	 *          errorMessage: null
	 *        };
	 *      }
	 *      case "FOOD_DELIVERY_FAIL": {
	 *        return {
	 *          ...state,
	 *          loading: false,
	 *          loaded: false,
	 *          errorMessage: action.errorMessage
	 *        };
	 *      }
	 *      default:
	 *        return state;
	 *    }
	 *  };
	 *
	 *  externalCodeSetup.reduxApi.addReducer("food", delivery)
	 *
	 */
	addReducer = (name, reducer) => {
		this.newReducers[name] = reducer;
	};

	/**
	 * Wraps original reducer with another reducer.
	 * It can be useful if you want to change original reducer behavior without replacing it.
	 * @method
	 * @param {AppReducers} name Name of reducer to wrap
	 * @param {ReducerWrapper} reducerWrapper
	 * @example <caption> Add a button which removes an item from the courses list </caption>
	 *
	 * //In custom_code/components/CourseItem.js
	 *
	 * import React from 'react';
	 * import { View, Text, TouchableOpacity, Button } from "react-native";
	 * import { WidgetItemCourseUserConnected } from "@src/components/Widgets/WidgetItemCourseUser";
	 * import { useDispatch } from "react-redux"
	 *
	 * const NewWidgetItemCourseComponent = (props) => {
	 *
	 *    const { viewModel, global, colors } = props;
	 *
	 *    const dispatch = useDispatch();
	 *
	 *    return <View>
	 *        <View style={{ margin: 20, flexDirection: "row" }}>
	 *
	 *            <TouchableOpacity onPress={viewModel.onClick}>
	 *                <Text>
	 *                    {viewModel.title}
	 *                </Text>
	 *
	 *                <WidgetItemCourseUserConnected
	 *                    lightText={true}
	 *                    global={global}
	 *                    userId={viewModel.authorId}
	 *                    colors={colors}
	 *                />
	 *
	 *            </TouchableOpacity>
	 *        </View>
	 *
	 *        <View>
	 *            //Use dispatch() from react-redux to dispatch an action
	 *            <Button title="Remove from list"
	 *            onPress={() => dispatch({ type: "COURSE_REMOVE_ITEM", courseToRemove: viewModel.id })} />
	 *        </View>
	 *
	 *    </View>
	 *
	 * }
	 *
	 * export default NewWidgetItemCourseComponent;
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 *
	 * import CourseItem from "./components/CourseItem"
	 * export const applyCustomCode = externalCodeSetup => {
	 *
	 *  externalCodeSetup.coursesHooksApi.setWidgetItemCourseComponent(CourseItem);
	 *
	 *   const reducerName = "courses"; // "courses" reducer can access data displayed in courses list
	 *
	 *   //Initialize the custom reducer
	 *   const customReducer = reducer => (state = reducer(undefined, {}), action) => {
	 *
	 *    switch (action.type) {
	 *
	 *      case "COURSE_REMOVE_ITEM":
	 *
	 *        //Use variables below to get index of id which will be removed from the list
	 *        const removeWithIndex = state.all.ids.indexOf(action.courseToRemove);
	 *        const newIds = state.all.ids.splice(removeWithIndex, 1)
	 *
	 *        //Assign new state with "newIds"
	 *        const newState = {
	 *          ...state,
	 *          all: {
	 *            ...state.all,
	 *            ids: newIds
	 *          }
	 *        }
	 *
	 *        return reducer(newState, action);
	 *
	 *      default:
	 *        return reducer(state, action);
	 *    }
	 *
	 *  }
	 *
	 *  externalCodeSetup.reduxApi.wrapReducer(
	 *    reducerName,
	 *    customReducer
	 *  );
	 * }
	 *
	 */
	wrapReducer = (name, reducerWrapper) => {
		this.reducerWrappers[name] = reducerWrapper;
	};

	/**
	 * It adds a new epic to the root epic.
	 * Note: The new epic will replace the old epic if “name” used to add an epic already exists.
	 * @method
	 * @param {String} name Epic name
	 * @param {Epic} epic
	 * @example <caption> Call an epic </caption>
	 *
	 * ...
	 *
	 * import { asObservable } from "@src/epics/rxUtils";
	 * const { getApi } = require("@src/services");
	 * import { errorToAction } from "@src/utils";
	 * export const applyCustomCode = externalCodeSetup => {
	 *
	 * const courseRemoveFail = (error) => ({
	 *  type: "COURSE_REMOVE_ERROR",
	 *  error,
	 * })
	 *  const courseRemoveItem = (
	 *    action$,
	 *    store
	 *  ) =>
	 *    action$
	 *      .filter(
	 *        a =>
	 *          a.type === "COURSE_REMOVE_ITEM" //Observer for type "COURSE_REMOVE_ITEM". If called, proceed with mergeMap function
	 *      )
	 *      .mergeMap(a => {
	 *        let state = store.getState();
	 *        let { config } = state;
	 *
	 *        //Create a "loading" observable
	 *        const loadingObservable = Observable.of({
	 *          type: "COURSE_REMOVE_LOADING"
	 *        });
	 *
	 *        const api = getApi(config);
	 *
	 *        const urlPath = "https://api-to-call.com",
	 *          method = "GET",
	 *          paramsOrPayload = {},
	 *          validation = {},
	 *          isfullUrl = true,
	 *          headers = {
	 *            appid: config.app_id
	 *          }
	 *
	 *        //Use BuddyBoss helper function "api.customRequest"
	 *        const apiToCall = api
	 *          .customRequest(
	 *            urlPath,
	 *            method,
	 *            paramsOrPayload,
	 *            validation,
	 *            headers,
	 *            isfullUrl
	 *          );
	 *
	 *        const requestObservable = asObservable(apiToCall)
	 *          .map(() => ({
	 *            type: "COURSE_REMOVE_SUCCESS"
	 *           }))
	 *          .catch(ex =>
	 *            Observable.of(
	 *             errorToAction(ex, e =>
	 *              courseRemoveFail(e)
	 *            )));
	 *        return Observable.concat(loadingObservable, requestObservable);
	 *
	 *      });
	 *
	 *  externalCodeSetup.reduxApi.addEpic("courseRemoveItem", courseRemoveItem);
	 * }
	 */
	addEpic = (name, epic) => {
		this.newEpics[name] = epic;
	};

	/**
	 * You can use it to wrap an epic with another epic.
	 * It can be used to filter action observables of existing epics.
	 * @method
	 * @param {AppEpic} name
	 * @param {EpicWrapper} epicWrapper
	 * @example <caption> Observes for type "CUSTOM_LOAD_COURSE_REQUEST" in "loadCoursesOnRequest" epic </caption>
	 * const filterActionsWrapper = (originalEpic: Epic): Epic =>
	 *  (
	 *    action,
	 *    storeApi,
	 *    dependencies
	 *  ) => {
	 *    const filteredInputActions =
	 *      action
	 *        .filter(
	 *          a =>
	 *            a.type === "CUSTOM_LOAD_COURSE_REQUEST"
	 *        )
	 *    return originalEpic(filteredInputActions, storeApi, dependencies);
	 *
	 * };
	 * externalCodeSetup.reduxApi.wrapEpic("loadCoursesOnRequest", filterActionsWrapper);
	 */
	wrapEpic = (name, epicWrapper) => {
		this.epicWrappers[name] = epicWrapper;
	};

	/**
	 * It can be used to execute a function once a redux store has been created.
	 * @method
	 * @param {Function} onCreated The function which will be called when redux store is created
	 * @returns {Function} Unsubscribe function
	 * @example
	 * externalCodeSetup.reduxApi.addOnStoreCreateListener((props) => {
	 *  //Redux store has been created!
	 *  //Do something here..
	 * })
	 */
	addOnStoreCreateListener = onCreated => {
		this.storeCreateListeners.push(onCreated);
		return () => {
			const index = this.storeCreateListeners.indexOf(onCreated);
			if (index !== -1) {
				this.storeCreateListeners.splice(index, 1);
			}
		};
	};

	/**
	 * You can use it to add a new middleware to store initialization
	 * @method
	 * @param {ReduxMiddleware} middleware
	 * @example
	 * import thunk from 'redux-thunk';
	 * ...
	 * externalCodeSetup.reduxApi.addMiddleware(thunk)
	 */
	addMiddleware = middleware => {
		this.newMiddlewares.push(middleware);
	};

	/**
	 * You can use this to set custom persistor configuration.
	 * It will replace the default configuration persistor from the app.
	 * Persist is used to store Redux state on disk so that it can be available after reopening the app without fetching data again.
	 * You can refer to this link for more information: {@link https://github.com/rt2zz/redux-persist}
	 * @method
	 * @param {Object} config - App persistor config
	 * @example <caption> Default configuration from app </caption>
	 *
	 * ...
	 *
	 * import AsyncStorage from "@react-native-community/async-storage";
	 *
	 * export const applyCustomCode = externalCodeSetup => {
	 *  const persistConfig = {
	 *   storage: AsyncStorage, // where to store
	 *   whitelist: [ // names of reducers to store
	 *     "auth",
	 *     "activities",
	 *     "blog",
	 *     "messages",
	 *     "forums",
	 *     "menuSettings",
	 *     "user",
	 *     "sites",
	 *     "settings",
	 *     "blockData",
	 *     "blockLoadableDataCache",
	 *     "activitiesCache",
	 *     "usersCache",
	 *     "topicsCache",
	 *     "forumsCache",
	 *     "coursesCache",
	 *     "groupsCache",
	 *     "messagesCache",
	 *     "blogCache",
	 *     "socialGroups",
	 *     "members",
	 *     "localAuthentication",
	 *     "urls",
	 *     "inAppPurchases",
	 *     "singleCourse",
	 *     "emailInvites"
	 *   ],
	 *   transforms: [reducerTransform, transformSerialiseImmutable],
	 *   debounce: 300,
	 *   key: "v5"
	 *  };
	 *
	 *  externalCodeSetup.reduxApi.overidePersistorConfig(persistConfig);
	 * }
	 */
	overidePersistorConfig = config => {
		this.customPersistorConfig = config;
	};

	/**
	 * You can use this to change config for redux persistor.
	 * For example, adding a new reducer to the whitelist.
	 * @method
	 * @param {Function} changer Function accepts the default persistor config; Or the changed persistor if `externalCodeSetup.reduxApi.overidePersistorConfig` is used.
	 * @returns {Object} PersistorConfig
	 * @example <caption>Add new reducer to whitelist </caption>
	 * externalCodeSetup.reduxApi.addPersistorConfigChanger(props => {
	 *  const changedPersistor = {
	 *   ...props,
	 *   whitelist: [
	 *     ...props.whitelist,
	 *     "customReducer"
	 *   ]
	 *  };
	 *  return changedPersistor
	 * })
	 */
	addPersistorConfigChanger = changer => {
		this.persitorConfigChangers.push(changer);
	};

	/**
	 * If set to `false`, redux data won't be saved to disk.
	 * @method
	 * @example
	 * externalCodeSetup.reduxApi.setIsDataPersistingEnabled(false)
	 */
	setIsDataPersistingEnabled = isEnabled => {
		this.isDataPersistingEnabled = isEnabled;
	};
}