Source

externalCode/topicSingle.js

  1. /**
  2. * @typedef {Object} TopicProps
  3. * @property { Object } global App global style
  4. * @property { Object } colors App colors
  5. * @property { TranslationFunction } t
  6. * @property {TopicViewModel} topic
  7. */
  8. /**
  9. * @typedef {Object} TopicTitleComponentProps
  10. * @property {TopicProps} topicProps
  11. */
  12. /**
  13. * @typedef {Object} TopicContentComponentProps
  14. * @property {TopicProps} topicProps
  15. * @property {Object} tagsStyles Styles used for different HTML tags
  16. * @property {String} content Topic content
  17. * @property {Function} attemptDeepLink Helper function to attempt deep link
  18. * @property {Number} computedWidth Default computed width of component to render
  19. */
  20. /**
  21. * @typedef {Object} TopicReplyButtonProps
  22. * @property {TopicProps} topicProps
  23. * @property {Boolean} topicCloseForUser Returns `true` if topic is closed for the logged-in user
  24. * @property {Function} openClosedDiscussionModal Helper function that shows a modal if the topic has been closed to new replies
  25. */
  26. /**
  27. * @typedef {Object} TopicMetadataComponentProps
  28. * @property {TopicProps} topicProps
  29. */
  30. /**
  31. * @typedef {Object} TopicItemHeaderProps
  32. * @property {TopicViewModel} item
  33. * @property {FormatDateFunction} formatDateFunc Function to help format date
  34. * @property {Object} global App global style
  35. * @property {String} textColor Default text color
  36. * @property {String} linkColor Default link color
  37. * @property {Boolean} light Returns `true` if text should be light colored
  38. * @property {String} alignItems Default alignment of items
  39. * @property {Number} avatarSize Default size of avatar
  40. * @property {Object} titleStyle Default styling for title
  41. * @property {Object} actionButtons Topic functions from redux
  42. */
  43. /**
  44. * @typedef {Object} ReplyItemAvatarProps
  45. * @property {Object} reply Reply details
  46. * @property {Object} global App global style
  47. * @property {Boolean} isNested Returns `true` if the reply is nested inside a reply
  48. */
  49. /**
  50. * @typedef {Object} ReplyItemHeaderProps
  51. * @property {Object} global App global style
  52. * @property {Object} headerTitleStyle Default styling applied to the component
  53. * @property {Object} reply Reply details
  54. * @property {Function} formatDateFunc Helper function which can be used to format dates
  55. */
  56. /**
  57. * @typedef {Object} ReplyItemContentProps
  58. * @property {Function} formatTextForDisplay Helper function which formats a text
  59. * @property {Function} filterContentCss Helper function which filters css to a safe format
  60. * @property {Object} reply Reply details
  61. * @property {Object} colors App colors
  62. * @property {TranslationFunction} t
  63. * @property {Object} global App global style
  64. * @property {Object} tagsStyles Default styling for HTML tags
  65. * @property {Object} imagesInitialDimensions
  66. * @property {Number} computedWidth
  67. * @property {String} referer Used by iframeRender to add `referer` to a webview
  68. * @property {Function} alterChildrenHTML Helper function which cleans up HTML
  69. * @property {Function} attemptDeepLink Helper function to attempt deep link
  70. * @property {Function} aTagRenderer Helper function for rendering anchor tags
  71. * @property {Function} iframeRender Helper function for rendering iFrame in an HTML
  72. *
  73. */
  74. /**
  75. * @class
  76. * Topic/Discussion Single Hooks.
  77. * Instance name: topicSingleApi
  78. You can use this to customize the default topic/discussion single screen components such as the ReplyItemAvatar, ReplyItemContent, and so on.
  79. * @example
  80. * externalCodeSetup.topicSingleApi.METHOD_NAME
  81. */
  82. export class TopicSingleApi {
  83. TopicItemHeader = null;
  84. /**
  85. * You can use this to customize the author name and date in the topic single screen.
  86. * @method
  87. * @param {React.ComponentType<TopicItemHeaderProps>} TopicItemHeader
  88. * @example <caption> Add a "Verified" text beside the author's name </caption>
  89. *
  90. * //In custom_code/components/TopicItemHeader.js...
  91. *
  92. * import React from "react";
  93. * import {View, Text, TouchableOpacity} from "react-native";
  94. * import {getAvatar} from "@src/utils";
  95. * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
  96. * import AppAvatar from "@src/components/AppAvatar";
  97. *
  98. * const renderVerified = (author) => {
  99. * if(author.id === 1){
  100. * return <Text style={{fontSize: 10}}>Verified</Text>
  101. * }
  102. *
  103. * return null;
  104. * }
  105. *
  106. * const ItemHeader = ({
  107. * item,
  108. * global,
  109. * formatDateFunc,
  110. * textColor,
  111. * linkColor,
  112. * light,
  113. * alignItems,
  114. * avatarSize,
  115. * titleStyle,
  116. * actionButtons
  117. * }) => {
  118. *
  119. * let lightStyle = {};
  120. * if (light) lightStyle = {color: "#ffffff"};
  121. *
  122. * let alignStyle = {};
  123. * if (alignItems) alignStyle = {alignItems: alignItems};
  124. * return (
  125. * <View style={[global.itemHeader, alignStyle]}>
  126. * <View style={[global.itemLeft, {alignItems: "center"}]}>
  127. * <AppTouchableOpacity
  128. * onPress={item.navigateToProfile ? item.navigateToProfile : () => {}}
  129. * style={global.avatarWrap}
  130. * >
  131. * <AppAvatar
  132. * size={avatarSize}
  133. * name={item.author.name}
  134. * source={{
  135. * uri: getAvatar(item.author.avatar, 96)
  136. * }}
  137. * />
  138. * </AppTouchableOpacity>
  139. * {!!item.author.name && (
  140. * <View style={{flex: 1}}>
  141. * <Text
  142. * style={[
  143. * global.itemName,
  144. * lightStyle,
  145. * titleStyle
  146. * ]}
  147. * >
  148. * {item.author.name} {renderVerified(item.author)}
  149. * </Text>
  150. * <View style={{flexDirection: "row", flexWrap: "wrap"}}>
  151. * <Text style={[global.itemMeta, lightStyle]}>
  152. * {formatDateFunc(item.lastActive)}
  153. * </Text>
  154. * </View>
  155. * </View>
  156. * )}
  157. * </View>
  158. * </View>
  159. * );
  160. * };
  161. *
  162. * export default ItemHeader;
  163. *
  164. * //In custom_code/index.js...
  165. *
  166. * ...
  167. *
  168. * import TopicItemHeader from "./components/TopicItemHeader";
  169. *
  170. * export const applyCustomCode = externalCodeSetup => {
  171. * externalCodeSetup.topicSingleApi.setTopicItemHeader( (props) => <TopicItemHeader {...props} />);
  172. * }
  173. */
  174. setTopicItemHeader = TopicItemHeader => {
  175. this.TopicItemHeader = TopicItemHeader;
  176. };
  177. ReplyItemAvatar = null;
  178. /**
  179. * You can use this hook to customize the reply item avatar in the topic single screen.
  180. * For example, you can add a "verified" icon or text beside the avatar.
  181. * @method
  182. * @param {React.ComponentType<ReplyItemAvatarProps>} ReplyItemAvatar
  183. * @example <caption> Add a "Verified" text below the avatar </caption>
  184. *
  185. * ...
  186. *
  187. * import AppAvatar from "@src/components/AppAvatar";
  188. * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
  189. *
  190. * export const applyCustomCode = externalCodeSetup => {
  191. *
  192. * externalCodeSetup.topicSingleApi.setReplyItemAvatar(props => {
  193. *
  194. * const { reply, global, isNested } = props
  195. *
  196. * return <AppTouchableOpacity
  197. * onPress={reply.navigateToProfile ? reply.navigateToProfile : () => { }}
  198. * style={global.avatarWrap}
  199. * >
  200. * <AppAvatar
  201. * size={isNested ? 30 : 40}
  202. * name={reply.author.fullname}
  203. * source={{
  204. * uri: reply.author.avatarUrl
  205. * }}
  206. * />
  207. * <Text>Verified</Text>
  208. * </AppTouchableOpacity>
  209. * })
  210. * }
  211. */
  212. setReplyItemAvatar = ReplyItemAvatar => {
  213. this.ReplyItemAvatar = ReplyItemAvatar;
  214. };
  215. ReplyItemHeader = null;
  216. /**
  217. * You can use this hook to customize the reply item header which by default contains the author of the reply and its date.
  218. * @method
  219. * @param {React.ComponentType<ReplyItemHeaderProps>} ReplyItemHeader
  220. * @example <caption> Change display date format </caption>
  221. *
  222. * ...
  223. * export const applyCustomCode = externalCodeSetup => {
  224. * externalCodeSetup.topicSingleApi.setReplyItemHeader(props => {
  225. * const { global, headerTitleStyle, reply, formatDateFunc } = props;
  226. * return <View style={global.row}>
  227. * <Text style={[global.itemName, headerTitleStyle, { marginBottom: 0 }]}>
  228. * {reply.author.fullname}
  229. * </Text>
  230. * {!reply.author.reported && (
  231. * <Text style={[global.itemLightMeta, { marginLeft: 8 }]}>
  232. * //{formatDateFunc(reply.date)}
  233. * {reply.date}
  234. * </Text>
  235. * )}
  236. * </View>
  237. *
  238. * })
  239. * }
  240. */
  241. setReplyItemHeader = ReplyItemHeader => {
  242. this.ReplyItemHeader = ReplyItemHeader;
  243. };
  244. ReplyItemContent = null;
  245. /**
  246. * You can use this hook to customize the reply item content.
  247. * @method
  248. * @param {React.ComponentType<ReplyItemContentProps>} ReplyItemContent
  249. * @example <caption> Implement default BB component </caption>
  250. *
  251. * //In custom_code/components/ReplyItemContent.js...
  252. *
  253. * import React from "react";
  254. * import {View} from "react-native";
  255. * import HTML from "react-native-render-html";
  256. * import ReadMore from "@src/components/ReadMore";
  257. * import AutoSizeImage from "@src/components/AutoSizeImage";
  258. * import ImageCollection from "@src/components/ImageCollection";
  259. * import {GifVideoPlayer} from "@src/components/Gif";
  260. * import EmbeddedDocumentItem from "@src/components/Documents/EmbeddedDocumentItem";
  261. *
  262. * const ReplyItemContent = ({
  263. * formatTextForDisplay,
  264. * filterContentCss,
  265. * reply,
  266. * colors,
  267. * t,
  268. * global,
  269. * tagsStyles,
  270. * imagesInitialDimensions,
  271. * computedWidth,
  272. * referer,
  273. * alterChildrenHTML,
  274. * attemptDeepLink,
  275. * aTagRenderer,
  276. * iframeRender
  277. * }) => (
  278. * <View style={{flex: 1, marginTop: 6}}>
  279. * <ReadMore
  280. * content={formatTextForDisplay(filterContentCss(reply.content))}
  281. * size={300}
  282. * colors={colors}
  283. * t={t}
  284. * global={global}
  285. * >
  286. * {content => (
  287. * <HTML
  288. * tagsStyles={{
  289. * ...tagsStyles,
  290. * p: {
  291. * ...tagsStyles.p,
  292. * marginBottom: 0
  293. * },
  294. * iframe: {
  295. * marginTop: 10
  296. * }
  297. * }}
  298. * baseFontStyle={global.textHtml}
  299. * html={content}
  300. * imagesInitialDimensions={imagesInitialDimensions}
  301. * staticContentMaxWidth={computedWidth}
  302. * alterChildren={alterChildrenHTML(computedWidth)}
  303. * onLinkPress={attemptDeepLink}
  304. * renderers={{
  305. * a: aTagRenderer(computedWidth),
  306. * iframe: iframeRender(referer),
  307. * img: (htmlAttribs, children, convertedCSSStyles, passProps) => {
  308. * return (
  309. * <AutoSizeImage
  310. * url={htmlAttribs.src}
  311. * wrapperStyle={{
  312. * marginTop: convertedCSSStyles.marginTop,
  313. * marginBottom: convertedCSSStyles.marginBottom
  314. * }}
  315. * style={{
  316. * ...convertedCSSStyles,
  317. * paddingVertical: 100
  318. * }}
  319. * />
  320. * );
  321. * }
  322. * }}
  323. * />
  324. * )}
  325. * </ReadMore>
  326. * {!!reply.media && (
  327. * <ImageCollection
  328. * item={reply}
  329. * containerStyle={{marginTop: 16}}
  330. * colors={colors}
  331. * global={global}
  332. * t={t}
  333. * toUserBasedOnSettings={() => reply.navigateToProfile()}
  334. * showActionButtons={false}
  335. * />
  336. * )}
  337. * {!!reply.videos && (
  338. * <ImageCollection
  339. * item={reply}
  340. * containerStyle={{marginTop: 16}}
  341. * colors={colors}
  342. * global={global}
  343. * t={t}
  344. * toUserBasedOnSettings={() => reply.navigateToProfile()}
  345. * showActionButtons={false}
  346. * />
  347. * )}
  348. * {!!reply.gif?.preview_url ? (
  349. * <View style={{marginTop: 16}}>
  350. * <GifVideoPlayer
  351. * url={reply.gif?.video_url}
  352. * poster={reply.gif?.preview_url}
  353. * width={computedWidth - 30}
  354. * containerStyle={{backgroundColor: "#F9F9F9"}}
  355. * />
  356. * </View>
  357. * ) : null}
  358. *
  359. * {reply?.documents?.length > 0 &&
  360. * reply.documents.map(item => {
  361. * const viewModel: DocumentViewModel = documentToViewModel(item);
  362. *
  363. * return (
  364. * <EmbeddedDocumentItem
  365. * {...{
  366. * t,
  367. * colors,
  368. * global,
  369. * token,
  370. * viewModel,
  371. * navigation
  372. * }}
  373. * />
  374. * );
  375. * })}
  376. * </View>
  377. * );
  378. *
  379. * export default ReplyItemContent
  380. *
  381. * //In custom_code/index.js...
  382. *
  383. * import ReplyItemContent from "./components/ReplyItemContent"
  384. * export const applyCustomCode = externalCodeSetup => {
  385. * externalCodeSetup.topicSingleApi.setReplyItemContent(props => <ReplyItemContent {...props} />)
  386. * }
  387. *
  388. */
  389. setReplyItemContent = ReplyItemContent => {
  390. this.ReplyItemContent = ReplyItemContent;
  391. };
  392. TopicTitleComponent = null;
  393. /**
  394. * You can use this hook to customize the title of the topic/discussion in the TopicSingleScreen.
  395. * @method
  396. * @param {TopicTitleComponentProps} TopicTitleComponent
  397. * @example
  398. * externalCodeSetup.topicSingleApi.setTopicTitleComponent(({
  399. * global,
  400. * topic
  401. * }) => <Text style={[global.topicSingleTitle, { marginBottom: 20, color: "red" }]}>
  402. * {topic.title}
  403. * </Text>
  404. * )
  405. */
  406. setTopicTitleComponent = TopicTitleComponent => {
  407. this.TopicTitleComponent = TopicTitleComponent;
  408. };
  409. TopicContentComponent = null;
  410. /**
  411. * You can use this hook to customize the content of the topic/discussion in the TopicSingleScreen.
  412. * @method
  413. * @param {TopicContentComponentProps} TopicContentComponent
  414. * @example
  415. *
  416. * ...
  417. *
  418. * import HTML from "react-native-render-html";
  419. * import ReadMore from "@src/components/ReadMore";
  420. * import {
  421. * alterChildrenHTML
  422. * } from "@src/utils";
  423. * import { aTagRenderer } from "@src/utils/htmlRender";
  424. * export const applyCustomCode = (externalCodeSetup: any) => {
  425. *
  426. * externalCodeSetup.topicSingleApi.setTopicContentComponent(({
  427. * colors,
  428. * content,
  429. * global,
  430. * t,
  431. * tagsStyles,
  432. * attemptDeepLink,
  433. * computedWidth,
  434. * topic
  435. * }) => {
  436. *
  437. * const newContent = `<a href="https://google.com/search?q=${topic.title}"> Press this to google the topic </a>`
  438. *
  439. * return <View style={{ marginTop: -4 }}>
  440. * <ReadMore
  441. * colors={colors}
  442. * content={content}
  443. * size={400}
  444. * t={t}
  445. * global={global}
  446. * style={{ marginBottom: 20 }}
  447. * >
  448. * {content => (
  449. * <HTML
  450. * html={content + newContent}
  451. * tagsStyles={{
  452. * ...tagsStyles,
  453. * iframe: {
  454. * marginTop: 10,
  455. * marginBottom: 10
  456. * }
  457. * }}
  458. * baseFontStyle={global.textHtml}
  459. * onLinkPress={attemptDeepLink}
  460. * staticContentMaxWidth={computedWidth}
  461. * alterChildren={alterChildrenHTML(computedWidth)}
  462. * renderers={{
  463. * a: aTagRenderer(computedWidth)
  464. * }}
  465. * />
  466. * )}
  467. * </ReadMore>
  468. * </View>
  469. * })
  470. * }
  471. */
  472. setTopicContentComponent = TopicContentComponent => {
  473. this.TopicContentComponent = TopicContentComponent;
  474. };
  475. TopicReplyButton = null;
  476. /**
  477. * You can use this hook to customize the reply button which allows the users to reply to the main content of the topic/discussion.
  478. * @method
  479. * @param {TopicReplyButtonProps} TopicReplyButton
  480. * @example
  481. * ...
  482. *
  483. * import Icon from "@src/components/Icon";
  484. * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
  485. * import AuthWrapper from "@src/components/AuthWrapper";
  486. * export const applyCustomCode = (externalCodeSetup: any) => {
  487. * externalCodeSetup.topicSingleApi.setTopicReplyButton(({
  488. * t,
  489. * colors,
  490. * global,
  491. * topic,
  492. * topicCloseForUser,
  493. * openClosedDiscussionModal
  494. * }) =>
  495. * <AuthWrapper>
  496. * <AppTouchableOpacity
  497. * activeOpacity={topicCloseForUser ? 0.5 : 1}
  498. * style={[global.itemFooter, { opacity: topicCloseForUser ? 0.5 : 1 }]}
  499. * onPress={
  500. * topicCloseForUser ? openClosedDiscussionModal : topic.newReply
  501. * }
  502. * hitSlop={{ top: 10, right: 20, bottom: 20, left: 20 }}
  503. * >
  504. * <Icon
  505. * icon={{fontIconName: "reply", weight: 400}}
  506. * webIcon={"IconReply"}
  507. * tintColor={colors.descLightTextColor}
  508. * style={{
  509. * width: 14,
  510. * height: 12
  511. * }}
  512. * />
  513. * <Text
  514. * style={[global.itemMeta, { marginLeft: 6, color: colors.textColor }]}
  515. * >
  516. * {t("topic:reply")}
  517. * </Text>
  518. * </AppTouchableOpacity>
  519. * </AuthWrapper>
  520. * )
  521. * }
  522. */
  523. setTopicReplyButton = TopicReplyButton => {
  524. this.TopicReplyButton = TopicReplyButton;
  525. };
  526. TopicMetadataComponent = null;
  527. /**
  528. * You can use this hook to customize the metadeta component in the TopicSingleScreen which by default, displays the number of members and replies in the discussion.
  529. * @method
  530. * @param {TopicMetadataComponentProps} TopicMetadataComponent
  531. * @example
  532. *
  533. * externalCodeSetup.topicSingleApi.setTopicMetadataComponent(({
  534. * global,
  535. * topic
  536. * }) =>
  537. * <View style={[global.itemFooterMeta, {marginLeft: "auto"}]}>
  538. * <Text style={global.itemMeta}>
  539. * {topic.voiceCount} • {topic.replyCount}
  540. * </Text>
  541. * </View>
  542. * )
  543. */
  544. setTopicMetadataComponent = TopicMetadataComponent => {
  545. this.TopicMetadataComponent = TopicMetadataComponent;
  546. };
  547. }