import { getRewardTransactions, getTargetedWidgetIdAndKind, addProgramListener, removeProgramListener, IProgramListenerCallbackArgs, WidgetCreatedEvent, getPostedWidgets, getWidgetsInteractions, IWidgetPayload, IWidgetKindInteractionsMap, IWidgetInteraction, IRewardTransaction, IWidgetEarnableReward, IPostedWidgetsPayload, PREDICTION_FOLLOW_UP_WIDGET_KIND, PREDICTION_WIDGET_ID_PROP, getWidgetInteractions, } from '@livelike/javascript'; import { useCallback, useEffect, useRef, useState } from 'react'; import { getSelectedOptionIndex, getWidgeUIPhase, getWidgetResultState, getWidgetRewardsFromRewardTransactions, widgetStoreActions, timelineWidgetStoreActions, } from '../store'; import { useApi } from './useApi'; import { WidgetMode } from '../types'; export type UseLoadTimelineWidgetEffectArg = { programId: string; mode: WidgetMode; }; export function useLoadTimelineWidgetEffect({ programId, mode, }: UseLoadTimelineWidgetEffectArg) { const [moreWidgets, setMoreWidgets] = useState(false); const nextIteratorRef = useRef<() => Promise>>(); const { onApi, isLoading, error, data } = useApi(() => getPostedWidgets({ programId }) .then((widgetsPayload) => { if (!widgetsPayload.done) { setMoreWidgets(true); nextIteratorRef.current = widgetsPayload.next; } return widgetsPayload; }) .then(getWidgetsDetails) ); useEffect(() => { if (mode === WidgetMode.POPUP) { timelineWidgetStoreActions.updateTimelineWidgetStateAction({ programId, widgetTimelineState: { widgets: [], }, }); return; } onApi().then((apiData) => updateWidgetsState(apiData, programId)); }, [programId, mode]); useEffect(() => { if (!data && mode === WidgetMode.INTERACTIVE_TIMELINE) { return; } function onProgramListener({ event, message, }: IProgramListenerCallbackArgs) { if (Object.values(WidgetCreatedEvent).includes(event)) { const { id: widgetId, kind: widgetKind } = message; const widgetPayload = message; (PREDICTION_FOLLOW_UP_WIDGET_KIND.includes(widgetKind) ? getWidgetInteractions({ widgetId, widgetKind, interactionUrl: widgetPayload.widget_interactions_url_template, }) : Promise.resolve([]) ).then((widgetInteractions) => { widgetStoreActions.updateWidgetStateAction({ widgetId, widgetState: { widgetPayload, widgetInteractions, widgetResultState: getWidgetResultState({ widgetPayload, widgetInteractions, }), widgetUIPhase: getWidgeUIPhase({ widgetPayload, widgetInteractions, }), selectedOptionIndex: getSelectedOptionIndex({ widgetPayload, widgetInteractions, }), isTimelineWidget: true, }, }); timelineWidgetStoreActions.updateTimelineWidgetsAction({ programId, widgets: [{ widgetId, widgetKind }], prepend: true, }); }); } } addProgramListener({ programId }, onProgramListener); return () => { removeProgramListener({ programId }, onProgramListener); }; }, [data, programId, mode]); const onLoadMore = useCallback(() => { if (nextIteratorRef.current) { return nextIteratorRef .current() .then((res) => { if (res.done) { setMoreWidgets(false); nextIteratorRef.current = null; } return res.value; }) .then(getWidgetsDetails) .then((widgetsData) => updateWidgetsState(widgetsData, programId)); } }, [nextIteratorRef.current, programId]); return { onApi, isLoading, error, data, onLoadMore: moreWidgets ? onLoadMore : null, }; } type IWidgetDetails = [ IWidgetPayload[], IWidgetKindInteractionsMap, IRewardTransaction[], ]; const getWidgetsDetails = ( widgetsPayload: IPostedWidgetsPayload ): Promise => { return Promise.all([ getWidgetsInteractions({ interactionUrl: widgetsPayload.widget_interactions_url_template, }), Promise.all( widgetsPayload.widgets.map((widget) => getTargetedWidgetIdAndKind({ widget }) ) ).then((widgetIdsAndKinds) => getAllRewardTransasctions( widgetIdsAndKinds.map(({ widgetId }) => widgetId) ) ), ]).then(([widgetInteractions, widgetRewards]) => [ widgetsPayload.widgets, widgetInteractions, widgetRewards, ]); }; const updateWidgetsState = ( [widgets, widgetInteractions, widgetRewards]: IWidgetDetails, programId: string ) => { const { widgetRecords: widgetsPayloadRecord, followUpWidgetRecords } = getWidgetPayloadRecord(widgets); const widgetsInteractionsRecord = getWidgetsInteractionsRecord( widgetInteractions, followUpWidgetRecords ); const widgetsRewardsRecord = getWidgetsRewardsRecord( widgetRewards, followUpWidgetRecords ); // set individual widget state so that widget details are not reloaded // This is done to avoid sending multiple widget details request for every widget rendered // that would give same response. Object.keys(widgetsPayloadRecord).forEach((widgetId) => { const widgetPayload = widgetsPayloadRecord[widgetId]; const widgetInteractions = widgetsInteractionsRecord[widgetId] ?? []; widgetStoreActions.updateWidgetStateAction({ widgetId, widgetState: { widgetPayload, widgetInteractions, widgetRewards: widgetsRewardsRecord[widgetId], widgetResultState: getWidgetResultState({ widgetPayload, widgetInteractions, }), widgetUIPhase: getWidgeUIPhase({ widgetPayload, widgetInteractions, }), selectedOptionIndex: getSelectedOptionIndex({ widgetPayload, widgetInteractions, }), isTimelineWidget: true, }, }); }); timelineWidgetStoreActions.updateTimelineWidgetsAction({ programId, widgets: widgets.map(({ id: widgetId, kind: widgetKind }) => ({ widgetId, widgetKind, })), }); }; // get all reward transactions incase there are multiple pages of // reward transactions const getAllRewardTransasctions = async (widgetIds: string[]) => { const response = await getRewardTransactions({ widgetIds, }); let results = response.results; let done = response.done; while (!done) { const res = await response.next(); results = results.concat(res.value.results); done = res.done; } return results; }; // create a record of widgetId vs widgetpayload function getWidgetPayloadRecord(widgets: IWidgetPayload[]) { const followUpWidgetRecords = {}; const widgetRecords = widgets.reduce((record, widgetPayload) => { record[widgetPayload.id] = widgetPayload; if (PREDICTION_FOLLOW_UP_WIDGET_KIND.includes(widgetPayload.kind)) { followUpWidgetRecords[widgetPayload.id] = widgetPayload; } return record; }, {}); return { widgetRecords, followUpWidgetRecords }; } // create a record of widgetId vs widgetInteractions function getWidgetsInteractionsRecord( widgetsInteractions: IWidgetKindInteractionsMap, followUpWidgetRecords: Record ): Record { const record = {}; for (const interactions of Object.values(widgetsInteractions)) { for (const interaction of interactions) { record[interaction.widget_id] = [ ...(record[interaction.widget_id] ?? []), interaction, ]; } } // set followup widget interaction using its corresponding prediction widget id for (const predictionWidget of Object.values(followUpWidgetRecords)) { const widgetId = predictionWidget[PREDICTION_WIDGET_ID_PROP[predictionWidget.kind]]; if (widgetId && record[widgetId]) { record[predictionWidget.id] = record[widgetId]; } } return record; } // create a record of widgetId vs widgetRewards function getWidgetsRewardsRecord( widgetsRewards: IRewardTransaction[], followUpWidgetRecords: Record ): Record { const record = {}; for (const reward of widgetsRewards) { record[reward.widget_id] = [ ...(record[reward.widget_id] ?? []), ...getWidgetRewardsFromRewardTransactions([reward]), ]; } // set followup widget rewards using its corresponding prediction widget id for (const predictionWidget of Object.values(followUpWidgetRecords)) { const widgetId = predictionWidget[PREDICTION_WIDGET_ID_PROP[predictionWidget.kind]]; if (widgetId && record[widgetId]) { record[predictionWidget.id] = record[widgetId]; } } return record; }