UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

426 lines (419 loc) • 24.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); // @ts-nocheck const react_1 = tslib_1.__importStar(require("react")); const react_core_1 = require("@selfcommunity/react-core"); const styles_1 = require("@mui/material/styles"); const material_1 = require("@mui/material"); const react_intl_1 = require("react-intl"); const Skeleton_1 = require("../Skeleton"); const CustomAdv_1 = tslib_1.__importDefault(require("../CustomAdv")); const types_1 = require("@selfcommunity/types"); const utils_1 = require("@selfcommunity/utils"); const classnames_1 = tslib_1.__importDefault(require("classnames")); const pubsub_js_1 = tslib_1.__importDefault(require("pubsub-js")); const system_1 = require("@mui/system"); const Widget_1 = tslib_1.__importDefault(require("../Widget")); const InfiniteScroll_1 = tslib_1.__importDefault(require("../../shared/InfiniteScroll")); const VirtualizedScroller_1 = tslib_1.__importStar(require("../../shared/VirtualizedScroller")); const Feed_1 = require("../../constants/Feed"); const Pagination_1 = require("../../constants/Pagination"); const feed_1 = require("../../utils/feed"); const Footer_1 = tslib_1.__importDefault(require("../Footer")); const Skeleton_2 = tslib_1.__importDefault(require("./Skeleton")); const use_deep_compare_effect_1 = require("use-deep-compare-effect"); const StickyBox_1 = tslib_1.__importDefault(require("../../shared/StickyBox")); const constants_1 = require("./constants"); const messages = (0, react_intl_1.defineMessages)({ refresh: { id: 'ui.feed.refreshRelease', defaultMessage: 'ui.feed.refreshRelease' } }); const classes = { root: `${constants_1.PREFIX}-root`, left: `${constants_1.PREFIX}-left`, leftItems: `${constants_1.PREFIX}-left-items`, start: `${constants_1.PREFIX}-start`, headerItem: `${constants_1.PREFIX}-header-item`, end: `${constants_1.PREFIX}-end`, endMessage: `${constants_1.PREFIX}-end-message`, right: `${constants_1.PREFIX}-right`, refresh: `${constants_1.PREFIX}-refresh`, paginationLink: `${constants_1.PREFIX}-pagination-link` }; const Root = (0, styles_1.styled)(material_1.Grid, { name: constants_1.PREFIX, slot: 'Root' })(() => ({})); const PREFERENCES = [react_core_1.SCPreferences.ADVERTISING_CUSTOM_ADV_ENABLED, react_core_1.SCPreferences.ADVERTISING_CUSTOM_ADV_ONLY_FOR_ANONYMOUS_USERS_ENABLED]; /** * > API documentation for the Community-JS Feed component. Learn about the available props and the CSS API. * * * This component renders a feed. * Take a look at our <strong>demo</strong> component [here](/docs/sdk/community-js/react-ui/Components/Feed) #### Import ```jsx import {Feed} from '@selfcommunity/react-ui'; ``` #### Component Name The name `SCFeed` can be used when providing style overrides in the theme. #### CSS |Rule Name|Global class|Description| |---|---|---| |root|.SCFeed-root|Styles applied to the root element.| |left|.SCFeed-left|Styles applied to the left element.| |right|.SCFeed-right|Styles applied to the right element.| |end|.SCFeed-end|Styles applied to the end element.| |refresh|.SCFeed-refresh|Styles applied to the refresh section.| |paginationLink|.SCFeed-pagination-link|Styles applied to pagination links.| * * @param inProps */ const Feed = (inProps, ref) => { // PROPS const props = (0, system_1.useThemeProps)({ props: inProps, name: constants_1.PREFIX }); // HOOKS const intl = (0, react_intl_1.useIntl)(); const { id = 'feed', className, endpoint, endpointQueryParams = { limit: Pagination_1.DEFAULT_PAGINATION_LIMIT, offset: Pagination_1.DEFAULT_PAGINATION_OFFSET }, endMessage = (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.feed.noOtherFeedObject", defaultMessage: "ui.feed.noOtherFeedObject" }), refreshMessage = (0, jsx_runtime_1.jsx)(material_1.Typography, { dangerouslySetInnerHTML: { __html: `${intl.formatMessage(messages.refresh)}` } }), HeaderComponent, FooterComponent = Footer_1.default, FooterComponentProps = {}, widgets = [], ItemComponent, itemPropsGenerator, itemIdGenerator, ItemProps = {}, ItemSkeleton, ItemSkeletonProps = {}, onNextData, onPreviousData, FeedSidebarProps = {}, CustomAdvProps = {}, enabledCustomAdvPositions = [types_1.SCCustomAdvPosition.POSITION_FEED_SIDEBAR, types_1.SCCustomAdvPosition.POSITION_FEED], requireAuthentication = false, cacheStrategy = utils_1.CacheStrategies.NETWORK_ONLY, prefetchedData, scrollableTargetId, VirtualizedScrollerProps = {}, disablePaginationLinks = false, hidePaginationLinks = true, paginationLinksPageQueryParam = Pagination_1.DEFAULT_PAGINATION_QUERY_PARAM_NAME, PaginationLinkProps = {}, hideAdvs = false, emptyFeedPlaceholder } = props; // CONTEXT const scPreferences = (0, react_1.useContext)(react_core_1.SCPreferencesContext); const scUserContext = (0, react_1.useContext)(react_core_1.SCUserContext); // CONST const authUserId = scUserContext.user ? scUserContext.user.id : null; const limit = (0, react_1.useMemo)(() => endpointQueryParams.limit || Pagination_1.DEFAULT_PAGINATION_LIMIT, [endpointQueryParams]); const offset = (0, react_1.useMemo)(() => { if (prefetchedData) { const currentOffset = (0, utils_1.getQueryStringParameter)(prefetchedData.previous, 'offset') || 0; return prefetchedData.previous ? parseInt(currentOffset) + limit : 0; } return endpointQueryParams.offset || 0; }, [endpointQueryParams, prefetchedData]); // REF const isMountRef = (0, react_core_1.useIsComponentMountedRef)(); const containerRef = (0, react_1.useRef)(null); /** * Compute preferences */ const preferences = (0, react_1.useMemo)(() => { const _preferences = {}; PREFERENCES.map((p) => (_preferences[p] = scPreferences.preferences && p in scPreferences.preferences ? scPreferences.preferences[p].value : null)); return _preferences; }, [scPreferences.preferences]); // RENDER const theme = (0, styles_1.useTheme)(); const oneColLayout = (0, material_1.useMediaQuery)(theme.breakpoints.down('md'), { noSsr: (0, utils_1.isClientSideRendering)() }); const advEnabled = (0, react_1.useMemo)(() => preferences && preferences[react_core_1.SCPreferences.ADVERTISING_CUSTOM_ADV_ENABLED] && ((preferences[react_core_1.SCPreferences.ADVERTISING_CUSTOM_ADV_ONLY_FOR_ANONYMOUS_USERS_ENABLED] && scUserContext.user === null) || !preferences[react_core_1.SCPreferences.ADVERTISING_CUSTOM_ADV_ONLY_FOR_ANONYMOUS_USERS_ENABLED]), [preferences]); const prevWidgets = (0, react_core_1.usePreviousValue)(widgets); /** * Callback onNextPage * @param page * @param offset * @param total * @param data */ const onNextPage = (page, offset, total, data) => { setFeedDataLeft((prev) => prev.concat(_getFeedDataLeft(data, offset, total))); onNextData && onNextData(page, offset, total, data); }; /** * Callback onPreviousPage * @param page * @param offset * @param total * @param data */ const onPreviousPage = (page, offset, total, data) => { setFeedDataLeft((prev) => _getFeedDataLeft(data, offset, total).concat(prev)); // Remove item duplicated from headData if the page data already contains removeHeadDuplicatedData(data.map((item) => itemIdGenerator(item))); onPreviousData && onPreviousData(page, offset, total, data); }; // PAGINATION FEED const feedDataObject = (0, react_core_1.useSCFetchFeed)({ id, endpoint, endpointQueryParams: Object.assign(Object.assign({}, endpointQueryParams), { offset, limit }), onNextPage: onNextPage, onPreviousPage: onPreviousPage, cacheStrategy, prefetchedData }); /** * Compute Base Widgets */ const _widgets = (0, react_1.useMemo)(() => [ ...widgets, ...(advEnabled && enabledCustomAdvPositions.includes(types_1.SCCustomAdvPosition.POSITION_FEED_SIDEBAR) ? [ { type: 'widget', component: CustomAdv_1.default, componentProps: Object.assign({ position: types_1.SCCustomAdvPosition.POSITION_FEED_SIDEBAR }, CustomAdvProps), column: 'right', position: 0 } ] : []) ] .map((w, i) => Object.assign({}, w, { position: w.position * (w.column === 'right' ? 5 : 1), id: `${w.column}_${i}` })) .sort(feed_1.widgetSort), [widgets, advEnabled]); /** * Compute Widgets for the left column in a specific position */ const _getLeftColumnWidgets = (position = 1, total) => { const tw = { type: 'widget', component: CustomAdv_1.default, componentProps: Object.assign({ position: types_1.SCCustomAdvPosition.POSITION_FEED }, CustomAdvProps), column: 'left', position, id: `left_${position}` }; if (oneColLayout && !hideAdvs) { const remainingWidgets = position === total - 1 ? _widgets.filter((w) => w.position >= total) : []; return [ ..._widgets.filter((w) => w.position === position), ...(advEnabled && enabledCustomAdvPositions.includes(types_1.SCCustomAdvPosition.POSITION_FEED) && position > 0 && position % Feed_1.DEFAULT_WIDGETS_NUMBER === 0 ? [tw] : []), ...remainingWidgets ]; } const remainingWidgets = position === total - 1 ? _widgets.filter((w) => w.position >= total && w.column === 'left') : []; return [ ..._widgets.filter((w) => w.position === position && w.column === 'left'), ...(advEnabled && !hideAdvs && enabledCustomAdvPositions.includes(types_1.SCCustomAdvPosition.POSITION_FEED) && position > 0 && position % Feed_1.DEFAULT_WIDGETS_NUMBER === 0 ? [tw] : []), ...remainingWidgets ]; }; /** * Compute Widgets for the right column */ const _getRightColumnWidgets = () => { if (oneColLayout) { return []; } return _widgets.filter((w) => w.column === 'right'); }; /** * Get left column data * @param data */ const _getFeedDataLeft = (data, currentOffset, total) => { let result = []; if (total === 0) { result = oneColLayout ? _widgets : _widgets.filter((w) => w.column === 'left'); } else { data.forEach((e, i) => { result = result.concat([..._getLeftColumnWidgets(i + currentOffset, total), ...[e]]); }); } return result; }; /** * Get right column data */ const _getFeedDataRight = () => { return _getRightColumnWidgets(); }; // STATE const [feedDataLeft, setFeedDataLeft] = (0, react_1.useState)(prefetchedData ? _getFeedDataLeft(feedDataObject.results, feedDataObject.initialOffset, feedDataObject.count) : []); const [feedDataRight, setFeedDataRight] = (0, react_1.useState)(prefetchedData ? _getFeedDataRight() : []); const [headData, setHeadData] = (0, react_1.useState)([]); // REFS const refreshSubscription = (0, react_1.useRef)(null); const virtualScrollerState = (0, react_1.useRef)(null); const virtualScrollerMountState = (0, react_1.useRef)(false); // VIRTUAL SCROLL HELPERS const getScrollItemId = (0, react_1.useMemo)(() => (item) => item.type === 'widget' ? `${Feed_1.WIDGET_PREFIX_KEY}${item.id}` : `${item.type}_${itemIdGenerator(item)}`, []); /** * Callback on scroll mount */ const onScrollerMount = (0, react_1.useMemo)(() => () => { virtualScrollerMountState.current = true; }, []); /** * Callback on scroll mount */ const onScrollerStateChange = (0, react_1.useMemo)(() => (state) => { virtualScrollerState.current = state; }, []); /** * Callback on refresh */ const refresh = (0, react_1.useMemo)(() => () => { /** * Only if the feedDataObject is loaded reload data */ if (feedDataObject.componentLoaded) { setHeadData([]); setFeedDataLeft([]); setFeedDataRight(_getFeedDataRight()); feedDataObject.reload(); } }, [feedDataObject.componentLoaded, setHeadData, setFeedDataLeft, setFeedDataRight]); /** * Callback subscribe events * @param msg * @param data */ const subscriber = (msg, data) => { if (data.refresh) { refresh(); } }; /** * Render HeaderComponent */ const renderHeaderComponent = () => { return ((0, jsx_runtime_1.jsx)(material_1.Box, Object.assign({ className: classes.start }, { children: !feedDataObject.previous && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [virtualScrollerMountState.current && HeaderComponent, headData.map((item) => { const _itemId = `item_${itemIdGenerator(item)}`; return ((0, jsx_runtime_1.jsx)(ItemComponent, Object.assign({ className: classes.headerItem, id: _itemId }, itemPropsGenerator(scUserContext.user, item), ItemProps, { sx: { width: '100%' } }), _itemId)); })] })) }))); }; /** * Infinite scroll getNextPage */ const getNextPage = (0, react_1.useMemo)(() => () => { if (isMountRef.current && feedDataObject.componentLoaded && !feedDataObject.isLoadingNext) { feedDataObject.getNextPage(); } }, [isMountRef.current, feedDataObject.componentLoaded, feedDataObject.isLoadingNext]); /** * Infinite scroll getNextPage */ const getPreviousPage = (0, react_1.useMemo)(() => () => { if (isMountRef.current && feedDataObject.componentLoaded && !feedDataObject.isLoadingPrevious) { feedDataObject.getPreviousPage(); } }, [isMountRef.current, feedDataObject.componentLoaded, feedDataObject.isLoadingPrevious]); /** * Bootstrap initial data */ const _initFeedData = (0, react_1.useMemo)(() => () => { if (cacheStrategy === utils_1.CacheStrategies.CACHE_FIRST && feedDataObject.componentLoaded) { // Set current cached feed or prefetched data setFeedDataLeft(_getFeedDataLeft(feedDataObject.results, feedDataObject.initialOffset, feedDataObject.count)); setFeedDataRight(_getFeedDataRight()); } else if (!feedDataObject.componentLoaded) { // Load next page feedDataObject.getNextPage(); setFeedDataRight(_getFeedDataRight()); } }, [cacheStrategy, feedDataObject.componentLoaded, endpointQueryParams]); // EFFECTS (0, react_1.useEffect)(() => { /** * Initialize feed * Init feed data when the user is authenticated/un-authenticated * Use setTimeout helper to delay the request and cancel the effect * (ex. in strict-mode) if need it */ let _t; if ((requireAuthentication && authUserId !== null && !prefetchedData) || (!requireAuthentication && !prefetchedData)) { _t = setTimeout(_initFeedData); } return () => { _t && clearTimeout(_t); }; }, [requireAuthentication, authUserId, prefetchedData]); /** * If widgets changed, refresh the feed (it must recalculate the correct positions of the objects) */ (0, use_deep_compare_effect_1.useDeepCompareEffectNoCheck)(() => { if (prevWidgets && widgets && prevWidgets !== widgets) { refresh(); } }, [widgets]); /** * Subscribe/Unsubscribe for external events */ (0, react_1.useEffect)(() => { refreshSubscription.current = pubsub_js_1.default.subscribe(id, subscriber); return () => { pubsub_js_1.default.unsubscribe(refreshSubscription.current); }; }, [subscriber]); /** * Remove duplicated data when load previous page and * previously some elements have been added in the head */ const removeHeadDuplicatedData = (itemIds) => { setHeadData(headData.filter((item) => !itemIds.includes(itemIdGenerator(item)))); }; /** * Next page url * Useful for SSR and SEO */ const NextPageLink = (0, react_1.useMemo)(() => { return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: !disablePaginationLinks && feedDataObject.nextPage && ((0, jsx_runtime_1.jsx)(react_core_1.Link, Object.assign({ to: `?${paginationLinksPageQueryParam}=${feedDataObject.nextPage}`, className: (0, classnames_1.default)({ [classes.paginationLink]: hidePaginationLinks }) }, PaginationLinkProps, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.common.nextPage", defaultMessage: "ui.common.nextPage" }) }))) })); }, [feedDataObject.nextPage, disablePaginationLinks, paginationLinksPageQueryParam, hidePaginationLinks]); /** * Previous page url * Useful for SSR and SEO */ const PreviousPageLink = (0, react_1.useMemo)(() => { return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: !disablePaginationLinks && feedDataObject.previousPage && ((0, jsx_runtime_1.jsx)(react_core_1.Link, Object.assign({ to: `?${paginationLinksPageQueryParam}=${feedDataObject.previousPage}`, className: (0, classnames_1.default)({ [classes.paginationLink]: hidePaginationLinks }) }, PaginationLinkProps, { children: (0, jsx_runtime_1.jsx)(react_intl_1.FormattedMessage, { id: "ui.common.previousPage", defaultMessage: "ui.common.previousPage" }) }))) })); }, [feedDataObject.previousPage, disablePaginationLinks, paginationLinksPageQueryParam, hidePaginationLinks]); // EXPOSED METHODS (0, react_1.useImperativeHandle)(ref, () => ({ addFeedData: (data, syncPagination) => { // Use headData to save new items in the head of the feed list // In this way, the state of the feed (virtualScroller/cache) remains consistent setHeadData([...[data], ...headData]); if (syncPagination) { // Adding an element, re-sync next and previous of feedDataObject const nextOffset = parseInt((0, utils_1.getQueryStringParameter)(feedDataObject.next, 'offset') || feedDataObject.results.length - 1) + 1; const previousOffset = parseInt((0, utils_1.getQueryStringParameter)(feedDataObject.previous, 'offset') || offset) + 1; feedDataObject.updateState({ previous: (0, utils_1.updateQueryStringParameter)(feedDataObject.previous, 'offset', previousOffset), next: (0, utils_1.updateQueryStringParameter)(feedDataObject.next, 'offset', nextOffset), count: feedDataObject.count + 1 }); } }, refresh: () => { refresh(); }, getCurrentFeedObjectIds: () => { return [...headData.map((o) => o[o.type].id), ...feedDataObject.results.map((o) => o[o.type].id)]; } })); const InnerItem = (0, react_1.useMemo)(() => ({ state: savedState, onHeightChange, onStateChange, children: item }) => { const onItemHeightChange = () => { if (savedState && savedState.firstShownItemIndex !== undefined) { onHeightChange(); } }; const onItemStateChange = (state) => { onStateChange(Object.assign(Object.assign({}, savedState), state)); }; return ((0, jsx_runtime_1.jsx)(VirtualizedScroller_1.VirtualScrollChild, Object.assign({ onHeightChange: onItemHeightChange }, { children: item.type === 'widget' ? ((0, jsx_runtime_1.jsx)(item.component, Object.assign({ id: `${Feed_1.WIDGET_PREFIX_KEY}${item.position}` }, item.componentProps, (item.publishEvents && { publicationChannel: id }), savedState, { onStateChange: onItemStateChange, onHeightChange: onItemHeightChange }))) : ((0, jsx_runtime_1.jsx)(ItemComponent, Object.assign({ id: `item_${itemIdGenerator(item)}` }, itemPropsGenerator(scUserContext.user, item), ItemProps, { sx: { width: '100%' } }, savedState, { onStateChange: onItemStateChange, onHeightChange: onItemHeightChange }))) }))); }, []); if (feedDataObject.isLoadingNext && !feedDataLeft.length) { return ((0, jsx_runtime_1.jsx)(Skeleton_2.default, { children: [...Array(3)].map((v, i) => ((0, jsx_runtime_1.jsx)(ItemSkeleton, Object.assign({}, ItemSkeletonProps), i))) })); } return ((0, jsx_runtime_1.jsxs)(Root, Object.assign({ container: true, spacing: 2, id: id, className: (0, classnames_1.default)(classes.root, className) }, { children: [advEnabled && !hideAdvs && enabledCustomAdvPositions.includes(types_1.SCCustomAdvPosition.POSITION_BELOW_TOPBAR) ? ((0, jsx_runtime_1.jsx)(material_1.Grid, Object.assign({ item: true, xs: 12 }, { children: (0, jsx_runtime_1.jsx)(CustomAdv_1.default, Object.assign({ position: types_1.SCCustomAdvPosition.POSITION_BELOW_TOPBAR }, CustomAdvProps)) }))) : null, (0, jsx_runtime_1.jsx)(material_1.Grid, Object.assign({ item: true, xs: 12, md: 7 }, { children: (0, jsx_runtime_1.jsxs)(InfiniteScroll_1.default, Object.assign({ ref: containerRef, className: classes.left, dataLength: feedDataLeft.length, next: getNextPage, previous: getPreviousPage, hasMoreNext: Boolean(feedDataObject.next), hasMorePrevious: Boolean(feedDataObject.previous), header: PreviousPageLink, footer: NextPageLink, loaderNext: (0, jsx_runtime_1.jsx)(ItemSkeleton, Object.assign({}, ItemSkeletonProps)), loaderPrevious: (0, jsx_runtime_1.jsx)(ItemSkeleton, Object.assign({}, ItemSkeletonProps)), scrollThreshold: '90%', endMessage: (0, jsx_runtime_1.jsxs)(material_1.Box, Object.assign({ className: classes.end }, { children: [(0, jsx_runtime_1.jsx)(Widget_1.default, Object.assign({ className: classes.endMessage }, { children: (0, jsx_runtime_1.jsx)(material_1.CardContent, { children: endMessage }) })), FooterComponent ? (0, jsx_runtime_1.jsx)(FooterComponent, Object.assign({}, FooterComponentProps)) : null] })), refreshFunction: refresh, pullDownToRefresh: true, pullDownToRefreshThreshold: 1000, pullDownToRefreshContent: null, releaseToRefreshContent: (0, jsx_runtime_1.jsx)(material_1.Button, Object.assign({ color: "secondary", variant: "contained", className: classes.refresh }, { children: refreshMessage })), style: { overflow: 'visible' } }, (scrollableTargetId && { scrollableTarget: scrollableTargetId }), { children: [renderHeaderComponent(), feedDataObject.count === 0 && emptyFeedPlaceholder && emptyFeedPlaceholder, (0, jsx_runtime_1.jsx)(VirtualizedScroller_1.default, Object.assign({ className: classes.leftItems, items: feedDataLeft, itemComponent: InnerItem, onMount: onScrollerMount, onScrollerStateChange: onScrollerStateChange, getItemId: getScrollItemId, preserveScrollPosition: true, preserveScrollPositionOnPrependItems: true, cacheScrollStateKey: react_core_1.SCCache.getVirtualizedScrollStateCacheKey(id), cacheScrollerPositionKey: react_core_1.SCCache.getFeedSPCacheKey(id), cacheStrategy: cacheStrategy }, (scrollableTargetId && { getScrollableContainer: () => document.getElementById(scrollableTargetId) }), VirtualizedScrollerProps))] })) })), feedDataRight.length > 0 && !hideAdvs && ((0, jsx_runtime_1.jsx)(material_1.Hidden, Object.assign({ smDown: true }, { children: (0, jsx_runtime_1.jsx)(material_1.Grid, Object.assign({ item: true, xs: 12, md: 5 }, { children: (0, jsx_runtime_1.jsx)(StickyBox_1.default, Object.assign({ className: classes.right }, FeedSidebarProps, { children: (0, jsx_runtime_1.jsx)(react_1.default.Suspense, Object.assign({ fallback: (0, jsx_runtime_1.jsx)(Skeleton_1.GenericSkeleton, {}) }, { children: feedDataRight.map((d, i) => ((0, jsx_runtime_1.jsx)(d.component, Object.assign({}, d.componentProps), i))) })) })) })) })))] }))); }; exports.default = (0, react_1.forwardRef)(Feed);