import * as React from "react";
/**
* @typedef {Function} CoursePropsFilterCallback
* @param {Object} oldProps Course props
* @return {Object} New props
*/
/**
* @typedef {Object} CourseMaterialsProps
* @property { Object } global App global style
* @property { Object } colors App colors
* @property { TranslationFunction } t
* @property { HTML } materials Contents of course materials field
* @property { RNStyleValue } tagsStyles Defined styles for different tags
* @property { RNStyleValue } materialsStyles Defined styles for course materials
* @property { Function } baseFontStyle Function to set a font style
* @property {NavigationService} navigation
* @property {Function} onLinkPress Helper function that handles link press for HTML component
*/
/**
* @typedef {Object} HeaderAuthorComponentProps
* @property { Object } user Author details
* @property { Boolean } lightStyle Returns true if course is using cover image
* @property { Object } global App global style
* @property { Number } userId User id of course author
* @property { Function } onUserProfilePress
* @property { Function } loadingInProgress
*/
/**
* @typedef {Function} TransformCourseActionButtonsCallback
* @property {React.ComponentType} buttons Course action button
* @property {CourseViewModel} course
* @property {TranslationFunction} t
* @property {Object} colors App colors
* @property {Object} global App global style
* @property {Array<Object>} products Products for IAP
* @property {NavigationService} navigation
* @property {Function} startCourse Function to start a course
* @property {Function} continueCourse Function to continue a course
* @property {React.ComponentType} continueCourse Renders pricing for the course
*/
/**
* @typedef {Object} IsCourseStatusHiddenCallback
* @property {CourseViewModel} course
* @property {Boolean} hasStarted Returns `true` if course progress has started
* @property {Object} courseTree Course lessons, topics, quizzes
*/
/**
* @typedef {Object} IsCategoryTagsHiddenCallback
* @property {CourseViewModel} course
*/
/**
* @typedef {Object} IsCourseDescriptionHiddenCallback
* @property {CourseViewModel} course
*/
/**
* @typedef {Object} CourseTitleProps
* @property { CourseViewModel } course
* @property { Object } global App global style
* @property {Object} colors App colors
* @property { Boolean } lightStyle Returns true if course is using cover image
* @property { Object } opacity Returns an object generated by React Native Animated
*/
/**
* @typedef {Object} CourseHeaderDetailsProps
* @property { CourseViewModel } courseVM
* @property { Object } global App global style
* @property {Object} colors App colors
* @property {Object} labels Learndash labels
* @property {TranslationFunction} t
* @property {Number} lCount Lessons count
* @property {Number} t Learn Topics count
* @property {Number} qCount Quizzes count
* @property {NavigationService} navigation
* @property {String} learndashCourseParticipants Can be used to determine if participants should be shown in the component
*/
/**
* @typedef {Object} CourseHeaderRightProps
* @property {Object} global App global style
* @property {Object} colors App colors
* @property {TranslationFunction} t
* @property {CourseViewModel} courseVM
* @property {Boolean} isShowingOpenCourseForGuestUser Returns `true` if course should be open for guest user
* @property {String} contentStyle Can be used to indicate what DownloadStatusIndicator component style should use
*/
/**
* @class
* Single Course Screen Hooks.
* Instance name: courseSingleApi
Hooks to modify the individual courses’ pages.
* @example
* externalCodeSetup.courseSingleApi.METHOD_NAME
*/
export class CourseSingleApi {
/**
* @private
* @property {React.ComponentType<any>} headerAuthorRenderer
*/
headerAuthorRenderer = null;
/**
* @private
* @property {boolean} showLessonsCount
*/
showLessonsCount = true;
/**
* @private
* @property {boolean} showQuizzesCount
*/
showQuizzesCount = true;
/**
* Sets component for overriding the default course author component.
* @method
* @param {React.ComponentType<HeaderAuthorComponentProps>} renderer
* @example <caption>Add more details besides the author name</caption>
*
* //In custom_code/components/CourseAuthor.js
*
* import React from 'react';
* import { View, Text } from 'react-native';
* import AppAvatar from "@src/components/AppAvatar";
* import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
* import { getBestImageVariant } from "@src/utils/CCDataUtil";
* import { withUserClickHandler } from "@src/components/hocs/withUserClickHandler"; //Use BuddyBoss HOC for easier navigation
*
* const CourseAuthor = (props) => {
*
* const { user, global, lightStyle, toUserBasedOnSettings } = props;
*
* //Able navigate to profile of `user` because of wrapping component with HOC
* const boundCallback = React.useMemo(
* () => toUserBasedOnSettings.bind(null, null, user),
* [user]
* );
*
* //Use BuddyBoss getBestImageVariant helper function to get best image size for the component
* const userAvatarUrl = React.useMemo(
* () =>
* !!user
* ? user.avatar_urls
* ? getBestImageVariant(user.avatar_urls, 96)
* : user?.avatar?.thumb
* : "",
* [user]
* );
*
* return <View style={[global.row]}>
* <AppTouchableOpacity onPress={boundCallback}>
* <View>
* {user && (
* <AppAvatar
* size={26}
* name={user.name}
* source={{
* uri: userAvatarUrl
* }}
* />
* )}
* </View>
* </AppTouchableOpacity>
* <View style={{ marginLeft: 8 }}>
* {user && (
*
* <>
* <Text
* style={[
* global.itemAuthorName,
* { marginBottom: 1 },
* lightStyle ? { color: "white" } : {}
* ]}
* >
* {user?.name}
* </Text>
*
* <Text> {user?.member_rest.user_email} </Text>
* </>
*
* )}
* </View>
* </View>
* }
*
* export default withUserClickHandler(CourseAuthor);
*
* //In custom_code/index.js
*
* ...
*
* import CourseAuthor from "./components/CourseAuthor";
* export const applyCustomCode = externalCodeSetup => {
* externalCodeSetup.courseSingleApi.setHeaderAuthorRenderer((props) => <CourseAuthor {...props} />)
* }
*/
setHeaderAuthorRenderer = renderer => {
this.headerAuthorRenderer = renderer;
};
/**
* @private
* @property {CoursePropsFilterCallback} filterIncomingCourseProps
*/
filterIncomingCourseProps = props => props;
/**
* Sets the callback function `filterIncomingCourseProps` used for filtering props that are coming into Courses Single Screen component
* @param {CoursePropsFilterCallback} filterIncomingCourseProps
* @method
* @example <caption>Add a date object to incoming course props</caption>
* externalCodeSetup.courseSingleApi.setFilterIncomingCourseProps((props) => {
* return {
* ...props,
* date: new Date()
* }
* });
*/
setFilterIncomingCourseProps = filterIncomingCourseProps => {
this.filterIncomingCourseProps = filterIncomingCourseProps;
};
isCategoryTagsHidden = course => false;
/**
* You can use this to show or hide the category tags in the course single screen.
* @param {IsCategoryTagsHiddenCallback} isCategoryTagsHidden
* @method
* @example
*
* externalCodeSetup.courseSingleApi.setIsCategoryTagsHidden((course) => {
* return false;
* })
*/
setIsCategoryTagsHidden = isCategoryTagsHidden => {
this.isCategoryTagsHidden = isCategoryTagsHidden;
};
/**
* @deprecated
*/
// show number of lessons on course single screen beside the lesson title
setShowLessonsCount = () => (this.showLessonsCount = true);
/**
* @deprecated
*/
// show number of quizzes on course single screen beside the quizzes title
setShowQuizzesCount = () => (this.showQuizzesCount = true);
// Course Materials
CourseMaterialsComponent = null;
/**
* Sets a component that overrides the content added in course materials field found in BuddyBoss site > Learndash LMS > Courses > [Selected Course] > Settings.
* @method
* @param {React.ComponentType<CourseMaterialsProps>} CourseMaterialsComponent
* @example <caption>Use a different component depending on the logged-in user</caption>
*
* //In custom_code/components/CourseMaterial.js
*
* import React, { useState } from 'react';
* import { View, Text, Alert, Dimensions } from 'react-native';
* import { useSelector } from "react-redux";
* import HTML from "react-native-render-html";
* import { WebView } from 'react-native-webview';
* import {
* documentFilename,
* isInExtensionList,
* htmlHandleClicks
* } from "@src/utils";
* import { previewDocument } from "@src/utils/previewFiles";
* const ent = require("ent");
* import { useScreenProps } from "@src/utils/ScreenPropsProvider";
* const DEVICE_WIDTH = Dimensions.get("window").width;
*
* const CourseMaterial = (props) => {
*
* const { tagsStyles, materialsStyles, baseFontStyle, materials, navigation, t } = props;
* const user = useSelector((state) => state.user.userObject);
* const online = useSelector((state) => state.network.online);
* const { auth } = useScreenProps(['auth']);
*
* const [opening, setOpening] = useState(false);
* const [progress, setProgress] = useState(0);
*
* const onLinkPress = async (evt, url) => {
*
* //Get auth from available screen props
*
* //Checks if extension added in url is supported for preview
* if (isInExtensionList(url)) {
* return await previewDocument({
* url,
* localName: documentFilename(url),
* token: auth.token,
* t,
* beginCallback: () => {
* setProgress(0);
* setOpening(true);
* },
* progressCallback: setProgress,
* doneCallback: localFile => {
* setTimeout(() => {
* setOpening(false);
* }, 1000);
* },
* failCallback: () => {
* setTimeout(() => {
* setOpening(false);
* }, 1000);
* }
* });
* }
*
* if (online) {
* //If online, handle urls using app's helper function
* return htmlHandleClicks(navigation, false)(evt, url);
* }
*
* Alert.alert(t("course:materialRequiresConnection"));
* };
*
* return user.id === 1 ?
*
* //Use a new component for SpecialUserComponent
* <>
* <Text style={{marginBottom: 20}}> Welcome back {user.name}! We prepared the materials for you. </Text>
* <WebView source={{ uri: 'https://link-to-best-materials.com' }} style={{ width: "auto", height: 300 }} />
* </>
* :
* //Use app's default component for RegularUserComponent
* <HTML
* tagsStyles={{ ...tagsStyles, ...materialsStyles }}
* baseFontStyle={baseFontStyle(15)}
* html={ent.decode(materials)}
* imagesMaxWidth={DEVICE_WIDTH - 32}
* onLinkPress={onLinkPress}
* />
*
* }
*
* export default CourseMaterial;
*
* //In custom_code/index.js
*
* ...
*
* import CourseMaterial from "./components/CourseMaterial";
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setCourseMaterialsComponent((props) => <CourseMaterial {...props} /> )
*
* }
*/
setCourseMaterialsComponent = CourseMaterialsComponent => {
this.CourseMaterialsComponent = CourseMaterialsComponent;
};
transformCourseActionButtons = (
buttons,
course,
t,
colors,
global,
products,
navigation,
startCoure,
continueCourse
) => buttons;
/**
* You can transform the default course action buttons which are starting, buying or continuing a course by replacing it with your preferred action buttons.
* @param {TransformCourseActionButtonsCallback} transformCourseActionButtons
* @method
* @example <caption> Add more components for course action </caption>
*
* import CourseActionButton from "@src/components/Course/CourseActionButton";
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setTransformCourseActionButtons((
* CourseActionBtn,
* courseVM,
* t,
* colors,
* global,
* products,
* navigation,
* startCourse,
* continueCourse,
* priceComponentRender) => {
*
* const Info =
* <View style={{ paddingHorizontal: 20, paddingVertical: 10 }}>
* <Text>To continue the course, tap the button below</Text>
* </View>
*
* const Redirect =
* <View style={{
* paddingHorizontal: 20,
* paddingVertical: 16,
* flexDirection: "row",
* alignItems: "center",
* justifyContent: "space-between"
* }}>
* <CourseActionButton
* title={"Go to Courses screen"}
* onPress={() => navigation.navigate("CoursesScreen")}
* style={{backgroundColor: "cyan"}}
* />
* </View>
* return [Info, CourseActionBtn, Redirect];
* })
*
* }
*/
setTransformCourseActionButtons = transformCourseActionButtons => {
this.transformCourseActionButtons = transformCourseActionButtons;
};
isCourseStatusHidden = (course, hasStarted, courseTree) => false;
/**
* You can use this hook to show or hide the course status component.
* @param {IsCourseStatusHiddenCallback} isCourseStatusHidden
* @method
* @example <caption> Hide the course status component depending on the course title </caption>
* externalCodeSetup.courseSingleApi.setIsCourseStatusHidden((course, hasStarted, courseTree) => {
* if (course.title == "React Native"){
* return true;
* }
* return false;
* })
*/
setIsCourseStatusHidden = isCourseStatusHidden => {
this.isCourseStatusHidden = isCourseStatusHidden;
};
isCourseDescriptionHidden = course => false;
/**
* You can use this hook to show or hide the course description component.
* @param {IsCourseDescriptionHiddenCallback} isCourseDescriptionHidden
* @method
* @example <caption> Hide the course description depending on the course title </caption>
* externalCodeSetup.courseSingleApi.setIsCourseDescriptionHidden((course) => {
* if (course.title == "React Native"){
* return true;
* }
* return false;
* })
*/
setIsCourseDescriptionHidden = isCourseDescriptionHidden => {
this.isCourseDescriptionHidden = isCourseDescriptionHidden;
};
CourseTitleComponent = null;
/**
* You can use this to replace the course title component.
* For example, you can use this to change the title's font size.
* @method
* @param {React.ComponentType<CourseTitleProps>} CourseTitleComponent
* @example <caption>Change the title's font size depending on the course title</caption>
*
* import Animated from "react-native-reanimated";
*
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setCourseTitleComponent((props) => {
*
* const { course, global, colors, styles, lightStyle, opacity } = props;
*
* let fontSize = 34;
*
* if (course.title === "React Native") {
* fontSize = 20;
* }
*
* return <View>
* <Animated.Text
* style={[
* global.iosStyleScreenTitle,
* styles.title,
* lightStyle ? { color: "white" } : { color: colors.textColor },
* { opacity, fontSize: fontSize }
* ]}
* >
* {course.title}
* </Animated.Text>
* </View>
*
* });
* }
*/
setCourseTitleComponent = CourseTitleComponent => {
this.CourseTitleComponent = CourseTitleComponent;
};
CourseHeaderDetails = null;
/**
* You can use this hook to customize the Course Status component of the LearnDash course details if a course is not yet in progress.
* For example, you can use this to change the course's preview video, featured image or the "Course Includes" details.
* @method
* @param {React.ComponentType<CourseHeaderDetailsProps>} CourseHeaderDetails
* @example <caption> Implement default BB component </caption>
*
* ...
*
* import Icon from "@src/components/Icon";
* import AppImageBackground from "@src/components/AppImageBackground";
* import { CourseVideo } from "@src/components/Course/CourseStatus";
*
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setCourseHeaderDetails(props => {
*
* const {
* courseVM,
* global,
* labels,
* colors,
* t,
* lCount,
* tCount,
* qCount,
* navigation,
* learndashCourseParticipants
* } = props;
*
* const size = 26;
*
* const shouldShowParticipants = !!+learndashCourseParticipants;
*
* const showIncludesTitle = lCount !== 0 ||
* tCount !== 0 ||
* qCount !== 0 ||
* courseVM.certificate?.available
*
* const styles = StyleSheet.create({
* videoContainer: {
* borderTopLeftRadius: 10,
* borderTopRightRadius: 10,
* overflow: "hidden",
* ...(!shouldShowParticipants && {
* borderBottomLeftRadius: 10,
* borderBottomRightRadius: 10
* })
* },
*
* avatar: {
* width: size + 4,
* height: size + 4,
* borderRadius: size + 4 / 2,
* borderColor: colors.bodyFrontBg,
* borderWidth: 2
* },
* enrolledText: {
* ...global.textAlt,
* left: -8,
* fontSize: 13
* },
* courseDetailsText: {
* ...global.itemText,
* marginBottom: 10,
* marginLeft: 14
* }
*
* });
*
* return (
* <>
* <View
* style={styles.videoContainer}
* >
* {!!!courseVM.videoUrl &&
* !!courseVM.featuredUrl && (
* <AppImageBackground
* source={{ uri: courseVM.featuredUrl }}
* style={{ width: "100%", height: 200 }}
* />
* )}
* {typeof courseVM.videoUrl === "string" &&
* courseVM.videoUrl !== "" && (
* <CourseVideo
* url={courseVM.videoUrl}
* feature={courseVM.featuredUrl}
* global={global}
* colors={colors}
* navigation={navigation}
* />
* )}
* </View>
*
* <View
* style={{
* paddingHorizontal: 16,
* ...(shouldShowParticipants && { paddingVertical: 12 })
* }}
* >
* {courseVM?.members?.length > 0 &&
* shouldShowParticipants && (
* <View style={{ ...global.row }}>
* {courseVM.members
* .map(x => x.member_rest?.avatar?.thumb || "")
* .map((url, index) => (
* <AppAvatar
* key={url}
* style={[styles.avatar, { left: index === 0 ? 0 : -10 * index, zIndex: index }]}
* borderRadius={size / 2}
* size={size}
* source={{
* uri: url
* }}
* />
* ))}
* <Text style={styles.enrolledText}>
* {courseVM.enrolledMembers > 2
* ? `+${courseVM.enrolledMembers - 2} ${t("course:enrolled")}`
* : ""}
* </Text>
* </View>
* )}
* {showIncludesTitle && (
* <Text
* style={[
* global.courseIncludesTitle,
* {
* marginBottom: 15,
* marginTop: courseVM?.members?.length > 0 ? 20 : 0
* }
* ]}
* >
* {t("course:includesTitle", { label: labels.course })}
* </Text>
* )}
* {lCount !== 0 && (
* <View style={{ flexDirection: "row" }}>
* <Icon
* webIcon={"IconAndroidGroup"}
* tintColor={colors.descLightTextColor}
* icon={require("@src/assets/img/book.png")}
* />
* <Text
* style={styles.courseDetailsText}
* >
* {lCount === ""
* ? " " + labels.lessons
* : t("course:itemCount", {
* count: lCount,
* slabel: labels.lesson,
* plabel: labels.lessons
* })}
* </Text>
* </View>
* )}
* {tCount !== 0 && (
* <View style={{ flexDirection: "row" }}>
* <Icon
* webIcon={"IconAndroidGroup"}
* tintColor={colors.descLightTextColor}
* icon={require("@src/assets/img/topic.png")}
* />
* <Text
* style={styles.courseDetailsText}
* >
* {tCount === ""
* ? " " + labels.topics
* : t("course:itemCount", {
* count: tCount,
* slabel: labels.topic,
* plabel: labels.topics
* })}
* </Text>
* </View>
* )}
* {qCount !== 0 && (
* <View style={{ flexDirection: "row" }}>
* <Icon
* webIcon={"IconAndroidGroup"}
* tintColor={colors.descLightTextColor}
* icon={require("@src/assets/img/quiz.png")}
* />
* <Text
* style={styles.courseDetailsText}
* >
* {qCount === ""
* ? " " + labels.quiz
* : t("course:itemCount", {
* count: qCount,
* slabel: labels.quiz,
* plabel: labels.quizzes
* })}
* </Text>
* </View>
* )}
* {courseVM.certificate?.available && (
* <View style={{ flexDirection: "row" }}>
* <Icon
* webIcon={"IconAndroidGroup"}
* tintColor={colors.descLightTextColor}
* icon={require("@src/assets/img/small-certificate.png")}
* />
* <Text
* style={styles.courseDetailsText}
* >
* {t("course:certificate", { label: labels.course })}
* </Text>
* </View>
* )}
* </View>
* </>
* );
*
* })
* }
*/
setCourseHeaderDetails = CourseHeaderDetails => {
this.CourseHeaderDetails = CourseHeaderDetails;
};
HeaderRightComponent = null;
/**
* You can use this hook to customize the component on the top right corner of the CourseSingleScreen which the download icon usually occupies.
* @method
* @param {React.ComponentType<CourseHeaderRightProps>} HeaderRightComponent
* @example
* ...
*
* import AuthWrapper from "@src/components/AuthWrapper";
* import DownloadStatusIndicator from "@src/components/Downloader/DownloadStatusIndicator";
* import StatusBarWrapper from "@src/utils/StatusBarWrapper";
* import { DownloadTypes } from "@src/services/enums/downloads";
*
* export const applyCustomCode = (externalCodeSetup: any) => {
*
* externalCodeSetup.courseSingleApi.setHeaderRightComponent(({
* global,
* colors,
* t,
* courseVM,
* isShowingOpenCourseForGuestUser,
* contentStyle
* }) => (
* courseVM.hasAccess &&
* !courseVM.offlineDisabled && (
* <AuthWrapper
* actionOnGuestLogin={
* isShowingOpenCourseForGuestUser
* ? null
* : "showAuthScreen"
* }
* >
* <DownloadStatusIndicator
* lightStyle={
* contentStyle === StatusBarWrapper.lightStyle
* }
* postId={courseVM.id}
* postType={DownloadTypes.course}
* colors={colors}
* global={global}
* size={30}
* t={t}
* iconColor={colors.linkColor}
* containerStyle={{
* marginLeft: 15
* }}
* />
* </AuthWrapper>
* )
*
* ))
* }
*/
setHeaderRightComponent = HeaderRightComponent => {
this.HeaderRightComponent = HeaderRightComponent;
};
CourseContentTitleComponent = null;
/**
* You can use this to replace the course content title component.
* @method
* @property {CourseViewModel} course
* @property {Object} global App global style
* @property {Object} colors App colors
* @property {Object} labels Learndash labels
* @property {TranslationFunction} t
* @example <caption>Replace the course content title component</caption>
*
* import { Text } from "react-native";
*
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setCourseContentTitleComponent((props) => {
*
* return (
* <Text style={{fontSize: 24}}> {`${course.title} Chapters`} </Text>
* )
* });
* }
*/
setCourseContentTitleComponent = CourseContentTitleComponent => {
this.CourseContentTitleComponent = CourseContentTitleComponent;
};
CourseDescriptionComponent = null;
/**
* You can use this to replace the course description component.
* @method
* @property {CourseViewModel} course
* @property {Object} global App global style
* @property {Object} colors App colors
* @property {Object} labels Learndash labels
* @property {TranslationFunction} t
* @example <caption>Replace the course description component</caption>
*
* import { Text } from "react-native";
*
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setCourseDescriptionComponent((props) => {
*
* return (
* <Text style={{fontSize: 24}}> {`About ${course.title}`} </Text>
* )
* });
* }
*/
setCourseDescriptionComponent = CourseDescriptionComponent => {
this.CourseDescriptionComponent = CourseDescriptionComponent;
};
LessonProgressStepsComponent = null;
/**
* You can use this to replace the course lesson progress step component.
* @method
* @property {CourseViewModel} course
* @property {Object} item Object containing data such like lesson id, title, topics etc
* @property {Object} global App global style
* @property {Object} colors App colors
* @property {TranslationFunction} t
* @property {Number} count Number indicating total progress
* @property {Number} completedSteps Number indicating progress completed
* @example <caption>Replace the course lesson progress steps component</caption>
*
* import { Text } from "react-native";
*
* export const applyCustomCode = externalCodeSetup => {
*
* externalCodeSetup.courseSingleApi.setLessonProgressStepsComponent((props) => {
*
* const { count, completedSteps } = props
* const percentCompleted = Math.round((completedSteps/count) * 100);
*
* return (
* <Text style={{ color: percentCompleted === 100 ? 'green' : 'black'}}>
* {`${percentCompleted}%`}
* </Text>
* )
* });
* }
*/
setLessonProgressStepsComponent = LessonProgressStepsComponent => {
this.LessonProgressStepsComponent = LessonProgressStepsComponent;
};
}
Source