Source

externalCode/topicsApi.js

/**
 * @typedef {Function} TransformTopicViewModelCallback
 * @param {TopicViewModel} viewModel
 * @param {Object} topic
 * @return {Object} - new view model
 */

/**
 * @typedef {Object} TopicViewModel
 * @property {Number} id Topic id
 * @property {String} title Topic title
 * @property {String} shortContent Topic short content
 * @property {Object} author Details about topic creator
 * @property {String} replyCount Number of replies in the topic
 * @property {String} voiceCount Number of members replied in the topic
 * @property {String} date Date created
 * @property {Boolean} canSeeReplies Returns `true` if logged in user can see replies
 * @property {Boolean} canReply Returns `true` if logged in user can reply
 * @property {String} content HTML string of topic content
 * @property {Array} mediaItems Media items in the topic
 * @property {String} lastActive Date of the topic's last activity
 * @property {String} forumTitle Forum title of the topic's parent
 * @property {Function} navigateToForum Function to navigate to topic's forum
 * @property {Function} navigateToProfile Function to navigate to topic author's profile
 * @property {Function} newReply Function to open new reply screen
 * @property {Function} favourite Function to toggle topic as favourite
 */

/**
 * @typedef {Object} TopicItemProps
 * @property {Boolean} firstItem Returns `true` if data is the first item in the list
 * @property {TopicViewModel} topic
 * @property {FormatDateFunction} formatDateFunc Function to help format date
 * @property {Object} styles App global style and app colors
 * @property {Object} actionButtons Topic functions from redux
 * @property {TranslationFunction} t
 */

/**
 * @typedef {Function} TransformTopicSubFiltersFilterCallback
 * @param {Array<string>} filters Available filters: `activity`, `date`, `title`, `popular`
 * @return {Array<string>}
 */

/**
 * @typedef {Function} TransformTopicsParams
 * @param {FetchTopicsParams}
 * @return {Object}
 */

/**
 * @typedef {Object} FetchTopicsParams
 * @see {@link https://www.buddyboss.com/resources/api/#api-Forum_Topics-GetBBPTopics}
 * @property {Number} per_page Maximum number of items to be returned in result set.
 * @property {Number} page Current page of the collection.
 * @property {String} search Limit results to those matching a string.
 * @property {String} orderby Sort retrieved topics by parameter. Allowed values: `meta_value`, `date`, `ID`, `author`, `title`, `modified`, `parent`, `rand`, `popular`, `activity`
 * @property {String} order Designates ascending or descending order of topics. Allowed values: `asc`, `desc`
 * @property {Number} parent Forum ID to retrieve all the topics.
 *
 */

/**
 * @class
 * Topic/Discussion Hooks.
 * Instance name: topicsApi
  
   You can use this to customize the default Topic/Discussion option such as rendering a custom topic item in the list and other options.
 * @example
 * externalCodeSetup.topicsApi.METHOD_NAME
 */
export class TopicsApi {
	topicToViewModelFilter = (viewModel, topic) => viewModel;

	/**
	 * You can use it to set the callback function that can change an existing topic's view model object.
	 * @method
	 * @param {TransformTopicViewModelCallback} topicToViewModelFilter
	 * @example
	 * externalCodeSetup.topicsApi.setTopicToViewModelFilter((props, topic) => {
	 *  return {
	 *   ...props,
	 *   topicUpvotes: 30
	 *  };
	 * })
	 */
	setTopicToViewModelFilter = topicToViewModelFilter =>
		(this.topicToViewModelFilter = topicToViewModelFilter);

	TopicItemComponent = null;
	/**
	 * It renders a custom topic item component in the topic list.
	 * @method
	 * @param {?React.ComponentType<TopicItemProps>} TopicItemComponent
	 * @example <caption> Add more details to Discussion/Topic item </caption>
	 *
	 * //In custom_code/components/TopicItem.js
	 *
	 * import React from 'react';
	 * import { View, Text, StyleSheet, Animated } from 'react-native';
	 * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
	 * import {getAvatar} from "@src/utils";
	 * import AppAvatar from "@src/components/AppAvatar";
	 * import AuthWrapper from "@src/components/AuthWrapper";
	 * import ActionSheetButton from "@src/components/ActionButtons/ActionSheetButton";
	 * import {GUTTER} from "@src/styles/global";
	 *
	 * const TopicItem = (props) => {
	 *
	 *    const {topic, styles, actionButtons, formatDateFunc, t} = props;
	 *
	 *    const global = styles.global;
	 *    const colors = styles.colors;
	 *
	 *    let rootStyle;
	 *
	 *    if (topic.actionStates.sticky) rootStyle = [global.itemSticky];
	 *
	 *    if (!topic.actionStates.open) rootStyle = [global.itemClosed];
	 *
	 *
	 *    const Item = <AppTouchableOpacity onPress={topic.toSingle} style={[rootStyle]}>
	 *        <Animated.View
	 *            style={{
	 *                ...StyleSheet.absoluteFillObject
	 *            }}
	 *        />
	 *        <View
	 *            style={{
	 *                ...global.row,
	 *                flex: 1,
	 *                marginHorizontal: GUTTER
	 *            }}
	 *        >
	 *            <AppAvatar
	 *                size={42}
	 *                name={topic.author.name}
	 *                source={{
	 *                    uri: getAvatar(topic.author.avatar, 96)
	 *                }}
	 *                style={{ marginTop: 15, alignSelf: "flex-start" }}
	 *            />
	 *            <View
	 *                style={{
	 *                    ...global.bottomBorder,
	 *                ...global.row,
	 *                flex: 1,
	 *                marginLeft: 10
	 *            }}
	 *        >
	 *            <View
	 *                style={[
	 *                    {
	 *                        flex: 1,
	 *                        paddingTop: 15,
	 *                        paddingBottom: 14,
	 *                        paddingLeft: 0,
	 *                        paddingRight: 0
	 *                    }
	 *                ]}
	 *            >
	 *                <Text
	 *                    style={{
	 *                        ...global.itemTitle,
	 *                        paddingRight: 40,
	 *                        marginBottom: 3
	 *                    }}
	 *                    numberOfLines={2}
	 *                    ellipsizeMode={"tail"}
	 *                >
	 *                    {topic.title}
	 *                </Text>
	 *                <Text numberOfLines={1} ellipsizeMode={"tail"}>{topic.shortContent}</Text>
	 *                <View style={{ ...global.row, marginBottom: 5 }}>
	 *                    <Text style={global.itemMeta}>{topic.voiceCount}</Text>
	 *                    <View style={global.dotSep} />
	 *                    <Text style={global.itemMeta}>{topic.replyCount}</Text>
	 *               </View>
	 *                <Text style={{ ...global.textAlt, color: colors.descTextColor }}>
	 *                    {t("topics:lastActive", {
	 *                        date: formatDateFunc(topic.lastActive)
	 *                    })}
	 *                </Text>
	 *            </View>
	 *            <AuthWrapper actionOnGuestLogin={"hide"}>
	 *                <ActionSheetButton
	 *                    color={colors.textIconColor}
	 *                    object={topic}
	 *                    colors={colors}
	 *                    actionButtons={actionButtons}
	 *                    headerProps={{
	 *                        onClick: topic.toSingle,
	 *                        title: topic.title,
	 *                        description: t("topics:lastActive", {
	 *                            date: formatDateFunc(topic.lastActive)
	 *                        }),
	 *                        avatarSource: {
	 *                            uri: getAvatar(topic.author.avatar, 96)
	 *                        }
	 *                    }}
	 *                    global={global}
	 *                    t={t}
	 *                />
	 *            </AuthWrapper>
	 *        </View>
	 *      </View>
	 *    </AppTouchableOpacity>
	 *
	 *    return Item;
	 * }
	 *
	 * export default TopicItem;
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 *
	 * import TopicItem from "./components/TopicItem";
	 * export const applyCustomCode = externalCodeSetup => {
	 *   externalCodeSetup.topicsApi.setTopicItemComponent(props => {
	 *     return <TopicItem {...props} />;
	 *   })
	 * }
	 */
	setTopicItemComponent = TopicItemComponent => {
		this.TopicItemComponent = TopicItemComponent;
	};

	subFiltersFilter = filters => filters;

	/**
	 * You can use this to set the subfilter function to rearrange the order of the filters in the Discussion screen.
	 * For example, you can rearrange the labels such as "Date created", "Last Active" and more on the discussions screen under the search bar.
	 * @method
	 * @param {TransformTopicSubFiltersFilterCallback} subFiltersFilter
	 * @example <caption>User would like to use "Date Created" filter only</caption>
	 * externalCodeSetup.topicsApi.setSubFiltersFilter((filters) => {
	 *   return ["date"]; //available filters include "activity", "date", "title", "popular"
	 * })
	 */
	setSubFiltersFilter = subFiltersFilter => {
		this.subFiltersFilter = subFiltersFilter;
	};

	TopicItemHeader = null;
	/**
	 * You can use this to customize the author name and date in the topic single screen.
	 * @deprecated Please use the equivalent hook: topicSingleApi.setTopicItemHeader()
	 * @method
	 * @example <caption> Add a "Verified" text beside the author's name </caption>
	 *
	 * //In custom_code/components/TopicItemHeader.js...
	 *
	 * import React from "react";
	 * import {View, Text, TouchableOpacity} from "react-native";
	 * import {getAvatar} from "@src/utils";
	 * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
	 * import AppAvatar from "@src/components/AppAvatar";
	 *
	 * const renderVerified = (author) => {
	 *    if(author.id === 1){
	 *        return <Text style={{fontSize: 10}}>Verified</Text>
	 *    }
	 *
	 *    return null;
	 * }
	 *
	 * const ItemHeader = ({
	 *  item,
	 *  global,
	 *  formatDateFunc,
	 *  textColor,
	 *  linkColor,
	 *  light,
	 *  alignItems,
	 *  avatarSize,
	 *  titleStyle,
	 *  actionButtons
	 * }) => {
	 *
	 * let lightStyle = {};
	 * if (light) lightStyle = {color: "#ffffff"};
	 *
	 * let alignStyle = {};
	 * if (alignItems) alignStyle = {alignItems: alignItems};
	 * return (
	 *    <View style={[global.itemHeader, alignStyle]}>
	 *      <View style={[global.itemLeft, {alignItems: "center"}]}>
	 *        <AppTouchableOpacity
	 *          onPress={item.navigateToProfile ? item.navigateToProfile : () => {}}
	 *          style={global.avatarWrap}
	 *        >
	 *      <AppAvatar
	 *        size={avatarSize}
	 *        name={item.author.name}
	 *        source={{
	 *          uri: getAvatar(item.author.avatar, 96)
	 *        }}
	 *      />
	 *      </AppTouchableOpacity>
	 *        {!!item.author.name && (
	 *          <View style={{flex: 1}}>
	 *            <Text
	 *              style={[
	 *                global.itemName,
	 *                lightStyle,
	 *                titleStyle
	 *              ]}
	 *            >
	 *              {item.author.name} {renderVerified(item.author)}
	 *            </Text>
	 *            <View style={{flexDirection: "row", flexWrap: "wrap"}}>
	 *              <Text style={[global.itemMeta, lightStyle]}>
	 *                {formatDateFunc(item.lastActive)}
	 *              </Text>
	 *            </View>
	 *          </View>
	 *         )}
	 *        </View>
	 *    </View>
	 *  );
	 * };
	 *
	 * export default ItemHeader;
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 *
	 * import TopicItemHeader from "./components/TopicItemHeader";
	 *
	 * export const applyCustomCode = externalCodeSetup => {
	 *  externalCodeSetup.topicsApi.setTopicItemHeader( (props) => <TopicItemHeader {...props} />);
	 * }
	 */
	setTopicItemHeader = TopicItemHeader => {
		this.TopicItemHeader = TopicItemHeader;
	};

	fetchParamsFilter = params => params;

	/**
	 * It overrides the parameters that are used to fetch topics in the Topics screen so that you can make it as customizable as possible when calling its API.
	 * @method
	 * @param {TransformTopicsParams} fetchParamsFilter
	 *
	 * @example <caption> Create a custom filter in topics screen </caption>
	 *
	 * //In custom_code/components/TopicsFiltersCustom.js...
	 *
	 * import React, { useState } from "react";
	 * import { TextInput, View, Button, Text, Switch } from 'react-native'
	 * import { useDispatch } from "react-redux";
	 * import { topicsLoadRequest } from "@src/actions/topics";
	 * import { getExternalCodeSetup } from "@src/externalCode/externalRepo";
	 * import withGlobalStyles from "@src/components/hocs/withGlobalStyles";
	 *
	 * const hook = getExternalCodeSetup().topicsApi;
	 * const screenName = "topics";
	 *
	 * getExternalCodeSetup().indexScreenApiHooks.setHeaderHeight((defaultHeaderHeight, filterType, navigation) => {
	 *
	 *    if (filterType === "topics"){
	 *        return 300
	 *    }
	 *
	 *    return defaultHeaderHeight;
	 *
	 * });
	 *
	 * const filter = "all";
	 * const subfilters = {
	 *    orderby: "activity", //"meta_value", "date", "ID", "author", "title", "modified", "parent", "rand", "popular", "activity"
	 *    order: "desc"
	 * }
	 *
	 * const refresh = true; //Set to true to refresh list
	 * const searchTerm = ""
	 *
	 * const TopicsFiltersCustom = (props) => {
	 *
	 *    const { navigation, route, colors } = props;
	 *
	 *    const dispatch = useDispatch();
	 *
	 *    //If showing the matched screen, show custom filter before displaying list component
	 *    if (route?.params?.item?.object === screenName) {
	 *
	 *        const [tag, setTag] = useState(false);
	 *
	 *        const handleSubmit = () => {
	 *
	 *            //Set custom parameters before fetching
	 *            hook.setFetchParamsFilter((props) => {
	 *
	 *                //You can add more parameters such as "subject", "keyword" etc...
	 *                return {
	 *                    ...props,
	 *                    tag
	 *                }
	 *            })
	 *
	 *            //Dispatch redux action to call api using customized filters
	 *            dispatch(topicsLoadRequest(filter, subfilters, refresh, searchTerm));
	 *
	 *        }
	 *
	 *        return <View style={{ backgroundColor: colors.whiteColor, alignItems: "center", justifyContent: "center" }}>
	 *
	 *            <TextInput
	 *                style={{paddingHorizontal: 20, marginTop: 10, fontSize: 20}}
	 *                autoFocus
	 *                value={tag}
	 *                onChangeText={tag => setTag(tag)}
	 *                placeholder="Search for tag..."
	 *            />
	 *            <Button
	 *                onPress={() => handleSubmit()}
	 *                title="Filter"
	 *            />
	 *        </View>
	 *    }
	 *
	 *    return null;
	 *
	 * }
	 *
	 * export default withGlobalStyles(TopicsFiltersCustom);
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 *
	 * import TopicsFiltersCustom from "./components/TopicsFiltersCustom";
	 * export const applyCustomCode = externalCodeSetup => {
	 *    externalCodeSetup.filterScreenApiHooks.setAfterFilterComponent(TopicsFiltersCustom);
	 * }
	 */
	setFetchParamsFilter = fetchParamsFilter => {
		this.fetchParamsFilter = fetchParamsFilter;
	};

	ReplyItemAvatar = null;
	/**
	 * You can use this hook to customize the reply item avatar in the topic single screen.
	 * For example, you can add a "verified" icon or text beside the avatar.
	 * @deprecated Please use the equivalent hook: topicSingleApi.setReplyItemAvatar()
	 * @method
	 * @example <caption> Add a "Verified" text below the avatar </caption>
	 *
	 * ...
	 *
	 * import AppAvatar from "@src/components/AppAvatar";
	 * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
	 *
	 * export const applyCustomCode = externalCodeSetup => {
	 *
	 *   externalCodeSetup.topicsApi.setReplyItemAvatar(props => {
	 *
	 *     const { reply, global, isNested } = props
	 *
	 *     return <AppTouchableOpacity
	 *       onPress={reply.navigateToProfile ? reply.navigateToProfile : () => { }}
	 *       style={global.avatarWrap}
	 *     >
	 *       <AppAvatar
	 *         size={isNested ? 30 : 40}
	 *         name={reply.author.fullname}
	 *         source={{
	 *           uri: reply.author.avatarUrl
	 *         }}
	 *       />
	 *       <Text>Verified</Text>
	 *     </AppTouchableOpacity>
	 *   })
	 * }
	 */
	setReplyItemAvatar = ReplyItemAvatar => {
		this.ReplyItemAvatar = ReplyItemAvatar;
	};

	ReplyItemHeader = null;
	/**
	 * You can use this hook to customize the reply item header which by default contains the author of the reply and its date.
	 * @deprecated Please use the equivalent hook: topicSingleApi.setReplyItemHeader()
	 * @method
	 * @example <caption> Change display date format </caption>
	 *
	 * ...
	 * export const applyCustomCode = externalCodeSetup => {
	 *   externalCodeSetup.topicsApi.setReplyItemHeader(props => {
	 *     const { global, headerTitleStyle, reply, formatDateFunc } = props;
	 *     return <View style={global.row}>
	 *       <Text style={[global.itemName, headerTitleStyle, { marginBottom: 0 }]}>
	 *         {reply.author.fullname}
	 *       </Text>
	 *       {!reply.author.reported && (
	 *         <Text style={[global.itemLightMeta, { marginLeft: 8 }]}>
	 *           //{formatDateFunc(reply.date)}
	 *           {reply.date}
	 *         </Text>
	 *       )}
	 *     </View>
	 *
	 *   })
	 * }
	 */
	setReplyItemHeader = ReplyItemHeader => {
		this.ReplyItemHeader = ReplyItemHeader;
	};

	ReplyItemContent = null;
	/**
	 * You can use this hook to customize the reply item content.
	 * @deprecated Please use the equivalent hook: topicSingleApi.setReplyItemContent()
	 * @method
	 * @example <caption> Implement default BB component </caption>
	 *
	 * //In custom_code/components/ReplyItemContent.js...
	 *
	 * import React from "react";
	 * import {View} from "react-native";
	 * import HTML from "react-native-render-html";
	 * import ReadMore from "@src/components/ReadMore";
	 * import AutoSizeImage from "@src/components/AutoSizeImage";
	 * import ImageCollection from "@src/components/ImageCollection";
	 * import {GifVideoPlayer} from "@src/components/Gif";
	 * import EmbeddedDocumentItem from "@src/components/Documents/EmbeddedDocumentItem";
	 *
	 * import {
	 *   documentToViewModel
	 * } from "@src/utils";
	 *
	 * const ReplyItemContent = ({
	 *   content,
	 *   readMoreContentSize,
	 *   attachmentMarginTop,
	 *   formatTextForDisplay,
	 *   filterContentCss,
	 *   reply,
	 *   colors,
	 *   t,
	 *   global,
	 *   tagsStyles,
	 *   imagesInitialDimensions,
	 *   computedWidth,
	 *   referer,
	 *   alterChildrenHTML,
	 *   attemptDeepLink,
	 *   aTagRenderer,
	 *   iframeRender
	 * }) => (
	 *   <View style={{flex: 1, marginTop: 6}}>
	 *     <ReadMore
	 *       content={content}
	 *       size={readMoreContentSize}
	 *       colors={colors}
	 *       t={t}
	 *       global={global}
	 *     >
	 *       {content => (
	 *         <HTML
	 *           tagsStyles={{
	 *             ...tagsStyles,
	 *             div: {
	 *               ...tagsStyles.p,
	 *             },
	 *             iframe: {
	 *               marginTop: 10
	 *             }
	 *           }}
	 *           baseFontStyle={global.textHtml}
	 *           html={content}
	 *           imagesInitialDimensions={imagesInitialDimensions}
	 *           staticContentMaxWidth={computedWidth}
	 *           alterChildren={alterChildrenHTML(computedWidth)}
	 *           onLinkPress={attemptDeepLink}
	 *           renderers={{
	 *             a: aTagRenderer(computedWidth),
	 *             iframe: iframeRender(referer),
	 *             img: (htmlAttribs, children, convertedCSSStyles, passProps) => {
	 *               return (
	 *                 <AutoSizeImage
	 *                   url={htmlAttribs.src}
	 *                   wrapperStyle={{
	 *                     marginTop: convertedCSSStyles.marginTop,
	 *                     marginBottom: convertedCSSStyles.marginBottom
	 *                   }}
	 *                   style={{
	 *                     ...convertedCSSStyles,
	 *                     paddingVertical: 100
	 *                   }}
	 *                 />
	 *                );
	 *             }
	 *           }}
	 *          />
	 *       )}
	 *     </ReadMore>
	 *     {!!reply.media && (
	 *       <ImageCollection
	 *         item={reply}
	 *         containerStyle={{marginTop: attachmentMarginTop}}
	 *         colors={colors}
	 *         global={global}
	 *         t={t}
	 *         toUserBasedOnSettings={() => reply.navigateToProfile()}
	 *         showActionButtons={false}
	 *       />
	 *     )}
	 *     {!!reply.videos && (
	 *       <ImageCollection
	 *         item={reply}
	 *         containerStyle={{marginTop: attachmentMarginTop}}
	 *         colors={colors}
	 *         global={global}
	 *         t={t}
	 *         toUserBasedOnSettings={() => reply.navigateToProfile()}
	 *         showActionButtons={false}
	 *       />
	 *     )}
	 *     {!!reply.gif?.preview_url ? (
	 *       <View style={{marginTop: attachmentMarginTop}}>
	 *         <GifVideoPlayer
	 *           url={reply.gif?.video_url}
	 *           poster={reply.gif?.preview_url}
	 *           width={computedWidth - 30}
	 *           containerStyle={{backgroundColor: "#F9F9F9"}}
	 *         />
	 *       </View>
	 *     ) : null}
	 *
	 *     {reply?.documents?.length > 0 &&
	 *       reply.documents.map(item => {
	 *         const viewModel = documentToViewModel(item);
	 *
	 *         return (
	 *           <EmbeddedDocumentItem
	 *             {...{
	 *               t,
	 *               colors,
	 *               global,
	 *               token,
	 *               viewModel,
	 *               navigation
	 *             }}
	 *           />
	 *         );
	 *       })}
	 *     </View>
	 * );
	 *
	 * export default ReplyItemContent
	 *
	 * //In custom_code/index.js...
	 *
	 * import ReplyItemContent from "./components/ReplyItemContent"
	 * export const applyCustomCode = externalCodeSetup => {
	 *   externalCodeSetup.topicsApi.setReplyItemContent(props => <ReplyItemContent {...props} />)
	 * }
	 *
	 */
	setReplyItemContent = ReplyItemContent => {
		this.ReplyItemContent = ReplyItemContent;
	};
}