/** * WordPress dependencies */ import { subscribe, select, dispatch } from '@safe-wordpress/data'; /** * External dependencies */ import { groupBy, keys } from 'lodash'; import { store as NC_DATA } from '@nelio-content/data'; import { addDays, getValue, isDefined, setValue } from '@nelio-content/utils'; import type { CacheableItems, EditorialTask, ExternalEvent, InternalEvent, Post, PremiumItem, PremiumItemType, SocialMessage, SocialProfile, Url, UserId, } from '@nelio-content/types'; /** * Internal dependencies */ import { store as NC_CALENDAR } from './store'; const CACHED_PROFILES_KEY = 'cached-profiles'; loadCachedProfiles(); cacheUpdatedProfiles(); const CACHED_ITEMS_KEY = 'cached-calendar-items'; loadCachedItems(); cacheEntitiesOnStoreChange(); // ======= // HELPERS // ======= type CachedData< T > = { readonly version?: string; readonly data?: T; }; export type SummarizedCacheableItems = { readonly post?: ReadonlyArray< SummarizedPost >; readonly social?: ReadonlyArray< SummarizedSocialMessage >; readonly task?: ReadonlyArray< SummarizedEditorialTask >; readonly 'external-event'?: ReadonlyArray< ExternalEvent >; readonly 'internal-event'?: ReadonlyArray< InternalEvent >; } & Partial< Record< PremiumItemType, ReadonlyArray< PremiumItem > > >; type SummarizedPost = Pick< Post, | 'id' | 'author' | 'authorName' | 'date' | 'isSticky' | 'isRewrite' | 'canBeRewritten' | 'series' | 'status' | 'statusName' | 'taxonomies' | 'title' | 'type' | 'typeName' >; type SummarizedSocialMessage = Pick< SocialMessage, | 'id' | 'auto' | 'network' | 'recurrenceGroup' | 'status' | 'schedule' | 'source' | 'textComputed' | 'postAuthor' | 'postId' | 'profileId' | 'type' >; type SummarizedEditorialTask = Pick< EditorialTask, | 'id' | 'assigneeId' | 'dateDue' | 'color' | 'completed' | 'postAuthor' | 'postId' | 'task' >; function cache< T >( key: string, data: T ): void { const MONTH_IN_SECONDS = 30 * 24 * 60 * 60; setValue( key, { version: select( NC_DATA ).getPluginVersion(), data, }, 2 * MONTH_IN_SECONDS ); } function loadCachedProfiles() { const cachedProfiles = getValue< CachedData< ReadonlyArray< SocialProfile > > >( CACHED_PROFILES_KEY ); if ( cachedProfiles?.version === select( NC_DATA ).getPluginVersion() ) { void dispatch( NC_DATA ).receiveSocialProfiles( cachedProfiles.data ?? [] ); } } function cacheUpdatedProfiles() { const cachedProfiles = select( NC_DATA ).getSocialProfiles(); const unsubscribe = subscribe( () => { if ( ! select( NC_DATA ).hasFinishedResolution( 'getSocialProfiles' ) ) { return; } unsubscribe(); const profiles = select( NC_DATA ).getSocialProfiles(); cachedProfiles .filter( ( p ) => profiles.includes( p ) ) .forEach( ( p ) => dispatch( NC_DATA ).removeSocialProfile( p.id ) ); cache( CACHED_PROFILES_KEY, select( NC_DATA ).getSocialProfiles() ); }, NC_DATA ); } function loadCachedItems() { const summarizedCachedItems = getValue< CachedData< SummarizedCacheableItems > >( CACHED_ITEMS_KEY ); if ( summarizedCachedItems?.version === select( NC_DATA ).getPluginVersion() ) { const cachedItems: CacheableItems = { ...summarizedCachedItems.data, post: summarizedCachedItems.data?.post?.map( hydratePost ), social: summarizedCachedItems.data?.social?.map( hydrateSocialMessage ), task: summarizedCachedItems.data?.task?.map( hydrateTask ), }; // NOTE. Premium items will not show up, because hooks to summarize/render them are not yet in place. We should address this in the future. void dispatch( NC_CALENDAR ).insertCachedItems( cachedItems ); } } function cacheEntitiesOnStoreChange() { let prevEntities: unknown; subscribe( () => { const entities = select( NC_DATA ).getRawEntities(); if ( entities === prevEntities ) { return; } prevEntities = entities; const cacheableItems = getCacheableItems(); cache( CACHED_ITEMS_KEY, cacheableItems ); }, NC_DATA ); } function getCacheableItems(): SummarizedCacheableItems { const firstDay = getFirstCacheableDay(); const lastDay = getLastCacheableDay(); const allItems = select( NC_DATA ).getCacheableItemsInPeriod( firstDay, lastDay ); const itemsByType = { ...groupBy( allItems, 'type' ) }; const types = keys( itemsByType ) as ReadonlyArray< keyof SummarizedCacheableItems >; return types.reduce( ( r: SummarizedCacheableItems, type ) => { const items = itemsByType[ type ] ?? []; switch ( type ) { case 'post': const posts = items .map( ( i ) => select( NC_DATA ).getPost( i.id as Post[ 'id' ] ) ) .filter( isDefined ) .map( summarizePost ); return posts.length ? { ...r, post: posts } : r; case 'social': const messages = items .map( ( i ) => select( NC_DATA ).getSocialMessage( i.id as SocialMessage[ 'id' ] ) ) .filter( isDefined ) .map( summarizeSocialMessage ); return messages.length ? { ...r, social: messages } : r; case 'task': const tasks = items .map( ( i ) => select( NC_DATA ).getTask( i.id as EditorialTask[ 'id' ] ) ) .filter( isDefined ) .map( summarizeTask ); return tasks.length ? { ...r, task: tasks } : r; case 'external-event': const externalEvents = items .map( ( i ) => select( NC_DATA ).getExternalEvent( i.id as ExternalEvent[ 'id' ] ) ) .filter( isDefined ); return externalEvents.length ? { ...r, 'external-event': externalEvents } : r; case 'internal-event': const internalEvents = items .map( ( i ) => select( NC_DATA ).getInternalEvent( i.id as ExternalEvent[ 'id' ] ) ) .filter( isDefined ); return internalEvents.length ? { ...r, 'internal-event': internalEvents } : r; default: const premiumItems = items .map( ( i ) => select( NC_DATA ).getPremiumItem( i.type as PremiumItemType, i.id as PremiumItem[ 'id' ] ) ) .filter( isDefined ); return premiumItems.length ? { ...r, [ type ]: premiumItems } : r; } }, {} ); } function getFirstCacheableDay() { const firstDayOfMonth = select( NC_DATA ).getToday().substring( 0, 8 ) + '01'; return addDays( firstDayOfMonth, -7 ); } function getLastCacheableDay() { const firstDayOfMonth = select( NC_DATA ).getToday().substring( 0, 8 ) + '01'; return addDays( firstDayOfMonth, 60 ); } function summarizePost( post: Post ): SummarizedPost { return { id: post.id, author: post.author, authorName: post.authorName, date: post.date, isSticky: post.isSticky, isRewrite: post.isRewrite, canBeRewritten: post.canBeRewritten, series: post.series, status: post.status, statusName: post.statusName, taxonomies: post.taxonomies, title: post.title, type: post.type, typeName: post.typeName, }; } function hydratePost( post: SummarizedPost ): Post { return { ...post, content: '', customFields: {}, customPlaceholders: {}, editLink: '' as Url, excerpt: '', followers: [], imageId: 0, imageAltText: '', imageSrc: false, images: [], imageIds: [], imageAltTexts: [], networkImages: {}, networkImageIds: {}, networkImageAltTexts: {}, permalink: '' as Url, permalinks: {}, permalinkQueryArgs: [], permalinkTemplate: '', thumbnailSrc: false, viewLink: '' as Url, statistics: { engagement: {}, pageviews: {}, }, }; } function summarizeSocialMessage( message: SocialMessage ): SummarizedSocialMessage { return { id: message.id, auto: message.auto, network: message.network, postId: message.postId, profileId: message.profileId, recurrenceGroup: message.recurrenceGroup, schedule: message.schedule, status: message.status, source: message.source, textComputed: message.textComputed, type: message.type, }; } function hydrateSocialMessage( message: SummarizedSocialMessage ): SocialMessage { return { ...message, dateType: 'predefined-offset', dateValue: '0', postAuthor: message.postAuthor, text: message.textComputed, timeType: 'predefined-offset', timeValue: '0', }; } function summarizeTask( task: EditorialTask ): SummarizedEditorialTask { return { id: task.id, assigneeId: task.assigneeId, color: task.color, completed: task.completed, dateDue: task.dateDue, postAuthor: task.postAuthor, postId: task.postId, task: task.task, }; } function hydrateTask( task: SummarizedEditorialTask ): EditorialTask { return { ...task, assignerId: 0 as UserId, dateType: 'predefined-offset', dateValue: '0', timezone: 'UTC', }; }