import React, { Component } from 'react' import { withActions, registerComponent } from '@financial-times/x-interaction' import { normalisePost } from './normalisePost' import { dispatchEvent } from './dispatchEvent' import { PostTracker } from './post-tracker' import { LiveBlogPost } from '../LiveBlogPost' /* eslint-disable @typescript-eslint/no-explicit-any */ //TODO: CI-1975 remove "any" types // this has been ported from x-dash, and a lot of what is expected into these arguments will take a lot of digging // checks to see if there is already a post and if not inserts it. tracks with dispatchEvent const withLiveBlogWrapperActions = withActions({ insertPost(newPost: any, wrapper: any) { return (props: any) => { const normalisedNewPost = normalisePost(newPost) const newPostAlreadyExists = props.posts.find( (post: any) => post.id === normalisedNewPost.id ) if (!newPostAlreadyExists) { props.posts.unshift(normalisedNewPost) dispatchEvent(wrapper, 'LiveBlogWrapper.INSERT_POST', { post: normalisedNewPost, }) } return props } }, }) type LiveBlogWrapperTypes = { articleUrl: string //base url for the article posts: Array // a list of liveblog posts //TODO: prob useful extracted into a posts type and use instead of any ads?: any showShareButtons: boolean // condition to show shareButtons showShareLinkButton: boolean // condition to show shareLinkButton id: string //id of the liveblog package liveBlogWrapperElementRef?: React.RefObject | undefined // react ref, used by FT App postTrackerConfig: any //optional config for tracking post //TODO: Body has postTrackerConfig type which could be useful serialiser: any //TODO: keeps TS happy, but maybe not required? } type LiveBlogWrapperState = { postCount?: number; tracker?: any } class BaseLiveBlogWrapper extends Component< LiveBlogWrapperTypes, LiveBlogWrapperState > { state: LiveBlogWrapperState = {} componentDidUpdate() { if ( this.state.tracker && this.props.posts.length !== this.state.postCount ) { this.state.tracker.observeNewElements() this.setState({ postCount: this.props.posts.length }) } } componentWillUnmount() { if (this.state.tracker) { this.state.tracker.destroy() } } componentDidMount() { if (this.props.posts && this.props.posts.length && !this.state.tracker) { this.setUp() } } setUp() { const { onEntersViewport, onRead, onError, usePostTracker } = this.props.postTrackerConfig || {} if (!usePostTracker) { return } if ( typeof onEntersViewport !== 'function' || typeof onRead !== 'function' || typeof onError !== 'function' ) { console.error( 'onEntersViewport, onRead and onError callback functions are required to use Post tracker' ) return } const tracker = this.setUpPostTracking( { onEntersViewport, onRead, onError }, this.props.id ) this.setState({ tracker, }) } /** * * @param {Object} callbacks - Callbacks to be called by PostTracker class * @param {Function} callbacks.onEntersViewport - function to be called when the criteria for `view` in Post tracker has been fulfiled * @param {Function} callbacks.onRead - function to be called when the criteria for `read` in Post tracker has been fulfiled * @param {Function} callbacks.onError - function to be called when PostTracker encounters an error * @param {string} id * @returns {PostTracker} */ setUpPostTracking(callbacks: any, id: string) { /** * @type {import('./post-tracker').PostTrackerConfig} */ const config = { query: 'article[data-trackable="live-post"],article[data-trackable="pinned-post"]', minMillisecondsToReport: 5000, returnVisibleElement: true, observerUpdateEventString: 'LiveBlogWrapper.INSERT_POST', liveBlogWrapperQuery: `div[data-live-blog-wrapper-id="${id}"]`, liveBlogWrapper: this.props.liveBlogWrapperElementRef ? this.props.liveBlogWrapperElementRef.current : undefined, onEntersViewport: (event: any) => callbacks.onEntersViewport(event), onRead: (event: any) => callbacks.onRead(event), onError: (event: any) => callbacks.onError(event), } /** * @type {PostTracker} */ return new PostTracker(config) } render() { const { posts = [], ads = {}, articleUrl, showShareButtons, showShareLinkButton, id, liveBlogWrapperElementRef, } = this.props //TODO: CI-1975 these types can move out when we have a posts type properly defined and shared posts.sort( ( a: { publishedDate: any publishedTimestamp: any isPinned: boolean }, b: { publishedDate: any publishedTimestamp: any isPinned: boolean } ) => { if (a.isPinned && !b.isPinned) { return -1 // A is pinned, B is not, A should come first } if (b.isPinned && !a.isPinned) { return 1 // B is pinned, A is not, B should come first } // For posts both pinned or both not pinned, sort by date const timestampA = a.publishedDate || a.publishedTimestamp const timestampB = b.publishedDate || b.publishedTimestamp // Newer posts on top if (timestampA > timestampB) { return -1 } if (timestampB > timestampA) { return 1 } return 0 } ) const postElements = posts.map((post: any, index: number) => ( )) return (
{postElements}
) } } const LiveBlogWrapper = withLiveBlogWrapperActions(BaseLiveBlogWrapper) // This enables the component to work with x-interaction hydration registerComponent(LiveBlogWrapper, 'LiveBlogWrapper') export { LiveBlogWrapper }