/* eslint-disable no-shadow */ import AuthFrame from './AuthFrame'; import ContentBox from './components/ContentBox'; import PopupBox from './components/PopupBox'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import i18nLib from '@tryghost/i18n'; import setupGhostApi from './utils/api'; import {ActionHandler, SyncActionHandler, isSyncAction} from './actions'; import {AdminApi, setupAdminAPI} from './utils/adminApi'; import {AppContext, DispatchActionType, EditableAppContext} from './AppContext'; import {CommentsFrame} from './components/Frame'; import {useOptions} from './utils/options'; type AppProps = { scriptTag: HTMLElement; }; const App: React.FC = ({scriptTag}) => { const options = useOptions(scriptTag); const [state, setFullState] = useState({ initStatus: 'running', member: null, admin: null, comments: [], pagination: null, commentCount: 0, secundaryFormCount: 0, popup: null }); const iframeRef = React.createRef(); const api = React.useMemo(() => { return setupGhostApi({ siteUrl: options.siteUrl, apiUrl: options.apiUrl!, apiKey: options.apiKey! }); }, [options]); const [adminApi, setAdminApi] = useState(null); const setState = useCallback((newState: Partial | ((state: EditableAppContext) => Partial)) => { setFullState((state) => { if (typeof newState === 'function') { newState = newState(state); } return { ...state, ...newState }; }); }, [setFullState]); const dispatchAction = useCallback(async (action, data) => { if (isSyncAction(action)) { // Makes sure we correctly handle the old state // because updates to state may be asynchronous // so calling dispatchAction('counterUp') multiple times, may yield unexpected results if we don't use a callback function setState((state) => { return SyncActionHandler({action, data, state, api, adminApi: adminApi!, options}); }); return; } // This is a bit a ugly hack, but only reliable way to make sure we can get the latest state asynchronously // without creating infinite rerenders because dispatchAction needs to change on every state change // So state shouldn't be a dependency of dispatchAction setState((state) => { ActionHandler({action, data, state, api, adminApi: adminApi!, options}).then((updatedState) => { setState({...updatedState}); }).catch(console.error); // eslint-disable-line no-console // No immediate changes return {}; }); }, [api, adminApi, options]); // Do not add state or context as a dependency here -> infinite render loop const i18n = useMemo(() => { return i18nLib(options.locale, 'comments'); }, [options.locale]); const context = { ...options, ...state, t: i18n.t, dispatchAction: dispatchAction as DispatchActionType }; const initAdminAuth = async () => { if (adminApi || !options.adminUrl) { return; } try { const adminApi = setupAdminAPI({ adminUrl: options.adminUrl }); setAdminApi(adminApi); let admin = null; try { admin = await adminApi.getUser(); } catch (e) { // Loading of admin failed. Could be not signed in, or a different error (not important) // eslint-disable-next-line no-console console.warn(`[Comments] Failed to fetch current admin user:`, e); } setState({ admin }); } catch (e) { /* eslint-disable no-console */ console.error(`[Comments] Failed to initialize admin authentication:`, e); } }; /** Fetch first few comments */ const fetchComments = async () => { const dataPromise = api.comments.browse({page: 1, postId: options.postId}); const countPromise = api.comments.count({postId: options.postId}); const [data, count] = await Promise.all([dataPromise, countPromise]); return { comments: data.comments, pagination: data.meta.pagination, count: count }; }; /** Initialize comments setup once in viewport, fetch data and setup state*/ const initSetup = async () => { try { // Fetch data from API, links, preview, dev sources const {member} = await api.init(); const {comments, pagination, count} = await fetchComments(); const state = { member, initStatus: 'success', comments, pagination, commentCount: count }; setState(state); } catch (e) { /* eslint-disable no-console */ console.error(`[Comments] Failed to initialize:`, e); /* eslint-enable no-console */ setState({ initStatus: 'failed' }); } }; /** Delay initialization until comments block is in viewport */ useEffect(() => { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { initSetup(); if (iframeRef.current) { observer.unobserve(iframeRef.current); } } }); }, { root: null, rootMargin: '0px', threshold: 0.1 }); if (iframeRef.current) { observer.observe(iframeRef.current); } return () => { if (iframeRef.current) { observer.unobserve(iframeRef.current); } }; }, [iframeRef.current]); const done = state.initStatus === 'success'; return ( {state.comments.length > 0 ? : null} ); }; export default App;