import { isNothing, isSome } from "@jet/environment"; import * as validation from "@jet/environment/json/validation"; import * as models from "../../api/models"; import { asArrayOrEmpty, asBoolean, asDictionary, asInterface, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; import { editorialCardFromData } from "../../foundation/media/associations"; import * as mediaAttributes from "../../foundation/media/attributes"; import { attributeAsString, hasAttributes } from "../../foundation/media/attributes"; import * as mediaDataStructure from "../../foundation/media/data-structure"; import { relationshipData } from "../../foundation/media/relationships"; import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; import { URL } from "../../foundation/network/urls"; import { isAdPlacementEnabled } from "../ads/ad-common"; import { categoryArtworkData } from "../categories"; import { artworkFromApiArtwork, iconFromData } from "../content/content"; import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; import { addImpressionFields } from "../metrics/helpers/impressions"; import { nextPosition, popLocation, pushContentLocation } from "../metrics/helpers/location"; import { combinedRecoMetricsDataFromMetricsData } from "../metrics/helpers/util"; import * as onDevicePersonalization from "../personalization/on-device-personalization"; import * as color from "./../../foundation/util/color-util"; import { asNumber } from "@apple-media-services/media-api"; import { createTodayAdCard } from "./cards/today-ad-card-builder"; import { defaultTodayCardConfiguration, todayCardFromData } from "./today-card-util"; import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; import { EditorialBackgroundType, TodayCardMetricsDisplayStyle, TodayHeaderArtworkBehavior, TodaySectionHeaderArtworkPlacement, } from "./today-types"; /** * We try to flatten the today response so we can treat it as a single list * of items. These are the different item types in the flattened list. */ export var FlattenedTodayItemType; (function (FlattenedTodayItemType) { FlattenedTodayItemType["EditorialItem"] = "editorialItem"; FlattenedTodayItemType["EditorialItemGroup"] = "editorialItemGroup"; })(FlattenedTodayItemType || (FlattenedTodayItemType = {})); /** * Flatten the entire modules array from the TodayPage response into a single list of items. * @param objectGraph The dependency graph for the App Store * @param todayModules The today modules to flatten from the page response * @param onboardingCardsData The media api data for the onboarding cards, inserted into the flattened list * @param todayRecommendations The today recommendations result, inserted into the flattened list * @param adsResponse The response from ad platforms * @returns The flattened list of today items */ export function flattenTodayModules(objectGraph, todayModules, todayRecommendations, onboardingCardsData, adsResponse) { const flattenedItems = []; let insertedOnboardingCards = isNullOrEmpty(onboardingCardsData); // Retrieve our personalization data const personalizationDataContainer = personalizationDataContainerForTodayModules(objectGraph, todayModules); let absoluteFeedIndex = 0; for (const todayModule of todayModules) { if (isNullOrEmpty(todayModule.contents)) { continue; } const personalizedModuleDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "today", todayModule.contents, true, personalizationDataContainer); todayModule.contents = personalizedModuleDataResult.personalizedData; todayModule.onDevicePersonalizationProcessingType = personalizedModuleDataResult.processingType; let isFirstItemInModule = true; const moduleMetadata = { label: todayModule.label, title: todayModule.title, meta: todayModule.meta, date: todayModule.date, onDevicePersonalizationProcessingType: todayModule.onDevicePersonalizationProcessingType, isTodayForAppsModule: mediaDataStructure.isModuleTodayForApps(todayModule), }; if (!insertedOnboardingCards) { for (const onboardingCardData of onboardingCardsData) { flattenedItems.push({ type: FlattenedTodayItemType.EditorialItem, data: onboardingCardData, isDataHydrated: hasAttributes(onboardingCardData), isFirstItemInModule, moduleMetadata: { ...moduleMetadata }, containedAdSlots: [absoluteFeedIndex], }); } isFirstItemInModule = false; insertedOnboardingCards = true; absoluteFeedIndex += 1; } for (let moduleItem of todayModule.contents) { const onDeviceUseCase = asString(moduleItem, "meta.personalizationData.onDeviceUseCase"); switch (moduleItem.type) { case "editorial-items": if (isSome(onDeviceUseCase)) { // ODP personalization for Today Arcade stories. const storyData = todayRecommendations === null || todayRecommendations === void 0 ? void 0 : todayRecommendations.storyData(onDeviceUseCase); if (isSome(storyData)) { moduleItem = storyData; } } flattenedItems.push({ type: FlattenedTodayItemType.EditorialItem, data: moduleItem, isDataHydrated: hasAttributes(moduleItem), isFirstItemInModule, moduleMetadata: { ...moduleMetadata }, containedAdSlots: [absoluteFeedIndex], }); isFirstItemInModule = false; absoluteFeedIndex += 1; break; case "editorial-item-groups": const groupContents = asArrayOrEmpty(moduleItem, "meta.associations.recommendations.data"); if (isNullOrEmpty(groupContents)) { continue; } let storyGroupData; if (isSome(onDeviceUseCase)) { // ODP personalization for Today Arcade story groups. storyGroupData = todayRecommendations === null || todayRecommendations === void 0 ? void 0 : todayRecommendations.storyGroupData(onDeviceUseCase); } if (isSome(storyGroupData)) { moduleItem = storyGroupData; } else { // ODP personalization for in-app event story groups. const personalizedStoryGroupDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "today", groupContents, true, personalizationDataContainer); moduleItem["meta"]["associations"]["recommendations"]["data"] = personalizedStoryGroupDataResult.personalizedData; todayModule.onDevicePersonalizationProcessingType = personalizedStoryGroupDataResult.processingType; } flattenedItems.push({ type: FlattenedTodayItemType.EditorialItemGroup, data: moduleItem, isDataHydrated: hasAttributes(moduleItem), isFirstItemInModule, moduleMetadata: { ...moduleMetadata }, containedAdSlots: Array.from({ length: groupContents.length }, (key, value) => value + absoluteFeedIndex), }); isFirstItemInModule = false; absoluteFeedIndex += groupContents.length; break; default: break; } } } return flattenedItems; } /** * Whether the next content item in the flattened list is hydrated. * @param flattenedItems The flattened list of today items * @returns True if the next content item is hydrated, false otherwise */ export function nextFlattenedItemIsHydrated(flattenedItems) { for (const flattenedItem of flattenedItems) { switch (flattenedItem.type) { case FlattenedTodayItemType.EditorialItem: case FlattenedTodayItemType.EditorialItemGroup: return hasAttributes(flattenedItem.data); default: break; } } return false; } /** * Iterates through all the today modules, and creates a set of personalization * data that is targetted only to the contents of these modules. * * @param dataArray The input array of today modules. * @returns Any relevant OnDevicePersonalizaionData */ export function personalizationDataContainerForTodayModules(objectGraph, todayModules) { if (!onDevicePersonalization.isPersonalizationAvailable(objectGraph)) { return null; } // Here we iterate through all the modules, and look inside each content items meta for personalizationData. const appIds = new Set(); for (const todayModule of todayModules) { if (isNull(todayModule.contents)) { continue; } const appIdFromContent = (contentData) => { return asString(contentData, "meta.personalizationData.appId"); }; for (const contentData of todayModule.contents) { switch (contentData.type) { case "editorial-item-groups": const groupItems = asArrayOrEmpty(contentData.meta, "associations.recommendations.data"); for (const groupItem of groupItems) { const appId = appIdFromContent(groupItem); if (isDefinedNonNullNonEmpty(appId)) { appIds.add(appId.toString()); } } break; default: const appId = appIdFromContent(contentData); if (isDefinedNonNullNonEmpty(appId)) { appIds.add(appId.toString()); } break; } } } return onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, appIds); } // MARK: - Shelf Creation /** * @param objectGraph The dependency graph for the App Store * @param todayItem The today item that this card is contained in, this could be a single item or a story group item * @param todayCardData The MAPI data that will be used to create this today card * @param isAdEligible Whether the current card is being placed in a slot that was eligible for an ad * @param currentRowIndex The current row index this card is going to be in * @param metricsDisplayStyle: The display style for impressions and location * @param isHeroCard: Whether this is the first card in a hero story group * @returns The configuration to use when parsing a today card */ function createTodayCardConfiguration(objectGraph, todayCardData, isAdEligible, currentRowIndex, metricsDisplayStyle, isHeroCard) { var _a; const cardConfig = defaultTodayCardConfiguration(objectGraph); cardConfig.useOTDTextStyle = (_a = asBoolean(todayCardData, "meta.personalizationData.isOfTheDay")) !== null && _a !== void 0 ? _a : false; cardConfig.replaceIfAdPresent = asBoolean(todayCardData, "meta.personalizationData.replaceIfAdPresent"); cardConfig.isAdEligible = isAdEligible; cardConfig.currentRowIndex = currentRowIndex; cardConfig.metricsDisplayStyle = metricsDisplayStyle; cardConfig.isHeroCard = isHeroCard; if (objectGraph.client.isWeb) { cardConfig.prevailingCropCodes = { "defaultCrop": "sr", "editorialArtwork.dayCard": "grav.west", }; } return cardConfig; } /** * @param objectGraph The dependency graph for the App Store * @param pageContext The page context for the today page * @returns The currentRowIndex from the pageContext, this is only used on iPhone, otherwise return * null since we cant reliably know the row index */ function rowCountFromContext(objectGraph, pageContext) { if (!objectGraph.client.isPhone) { return undefined; } return pageContext.currentRowIndex; } /** * Update the page context with the current row count, this uses the groupDisplayStyle to determine * the display style for the cards in the current row * @param pageContext The page context for the today page we're parsing * @param currentGroupDisplayStyle The current groupDisplayItem from the TodayItem we're parsing * @param currentItemIndex The index of the current item in the shelf, that we just created a card * for, this method is called after creating a card */ function incrementRowIndexIfNecessary(pageContext, currentGroupDisplayStyle, currentItemIndex) { switch (currentGroupDisplayStyle) { case models.GroupDisplayStyle.Grid: if (currentItemIndex % 2 === 1) { pageContext.currentRowIndex++; } break; case models.GroupDisplayStyle.Hero: if (currentItemIndex === 0 || (currentItemIndex - 1) % 2 === 1) { pageContext.currentRowIndex++; } break; case models.GroupDisplayStyle.Standard: pageContext.currentRowIndex++; break; default: break; } } /** * Update the page context with the current metrics display style, this is based off the groupDisplayStyle * @param objectGraph The dependency graph for the App Store * @param pageContext The page context for the today page we're parsing * @param currentGroupDisplayStyle The current groupDisplayItem from the TodayItem we're parsing * @param currentItemIndex The index of the current item in the shelf, that we just created a card * for, this method is called after creating a card */ function updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, currentGroupDisplayStyle, currentItemIndex) { if (objectGraph.client.isPad) { pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; return; } switch (currentGroupDisplayStyle) { case models.GroupDisplayStyle.Grid: pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; break; case models.GroupDisplayStyle.Hero: if (currentItemIndex === 0) { pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; } else { pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; } break; case models.GroupDisplayStyle.Standard: pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; break; default: break; } } /** * @param objectGraph The app store object graph * @param data The data from MAPI for a single item in the today feed, group or single EI * @returns The group display style for the given data */ function groupDisplayStyleFromData(objectGraph, data) { var _a; if (data.type === "editorial-items") { return models.GroupDisplayStyle.Standard; } let groupDisplayStyle; const editorialCard = editorialCardFromData(data); if (hasAttributes(editorialCard)) { groupDisplayStyle = mediaAttributes.attributeAsString(editorialCard, "editorialItemGroupDisplayStyle"); } if (isNothing(groupDisplayStyle)) { groupDisplayStyle = (_a = mediaAttributes.attributeAsString(data, "displayStyle")) !== null && _a !== void 0 ? _a : models.GroupDisplayStyle.Standard; } return isGroupDisplayStyleSupported(objectGraph, groupDisplayStyle) ? groupDisplayStyle : models.GroupDisplayStyle.Standard; } /** * @param objectGraph The app dependency graph * @param displayStyle The display style to check support for * @returns Whether group display style is supported for this platform */ function isGroupDisplayStyleSupported(objectGraph, displayStyle) { if (isNothing(displayStyle)) { return false; } switch (displayStyle) { case models.GroupDisplayStyle.Grid: return objectGraph.client.isPhone; default: return true; } } /** * Determine if the current card is eligible for an ad, based on the parsed card count, and the * bag defined values for slots that can have ads * @param pageContext The page context for the today page * @returns Whether the current card is eligible for an ad */ function isTodayCardConfigurationAdEligible(pageContext) { if (pageContext.adLocation === pageContext.parsedCardCount) { return true; } if (isNothing(pageContext.eligibleAdLocations)) { return false; } return pageContext.eligibleAdLocations.includes(pageContext.parsedCardCount); } /** * @param objectGraph The dependency graph for the App Store * @param todayItem The flattened today item to create a shelf for * @param pageContext The page context for the today page * @returns The shelf for the given today item, or null if we cant create one */ export function todayShelfForEditorialItem(objectGraph, todayItem, pageContext) { var _a; const shelf = createTodayShelfWithItems(objectGraph, todayItem, pageContext, () => { var _a, _b, _c; const shelfItems = []; (_a = pageContext.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.updateContainerId((_b = pageContext.pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.containerIdForSlotIndex((_c = pageContext.parsedCardCount) !== null && _c !== void 0 ? _c : 0)); const groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, groupDisplayStyle, 0); const cardConfig = createTodayCardConfiguration(objectGraph, todayItem.data, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), pageContext.currentRowMetricsDisplayStyle, false); cardConfig.baseMetricsOptions = { recoMetricsData: recoMetricsFromTodayItem(todayItem), }; const editorialItemTodayCard = createTodayCardForPageContext(objectGraph, pageContext, cardConfig, todayItem.data); if (isNothing(editorialItemTodayCard)) { return shelfItems; } shelfItems.push(editorialItemTodayCard); nextPosition(pageContext.locationTracker); pageContext.parsedCardCount++; incrementRowIndexIfNecessary(pageContext, groupDisplayStyle, 0); return shelfItems; }); shelf.contentsMetadata = { type: "todaySection", debugSectionTypeIndicatorColor: todayItem.type === FlattenedTodayItemType.EditorialItemGroup ? color.named("systemGreen") : color.named("systemBlue"), groupDisplayStyle: models.GroupDisplayStyle.Standard, }; // If this is not the first item in the module, attempt to set the background if (!todayItem.isFirstItemInModule) { const editorialShelfBackgroundInfo = todaySectionEditorialBackground(objectGraph, todayItem); if (isSome(editorialShelfBackgroundInfo)) { shelf.background = editorialShelfBackgroundInfo.shelfBackground; if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.configuration)) { shelf.header.configuration.eyebrowColor = editorialShelfBackgroundInfo.eyebrowColor; shelf.header.configuration.titleColor = editorialShelfBackgroundInfo.titleColor; shelf.header.configuration.subtitleColor = editorialShelfBackgroundInfo.subtitleColor; } } } return shelf; } /** * @param objectGraph The dependency graph for the App Store * @param todayItem The flattened today item, of type EditorialItemGroup to create a shelf for * @param pageContext The page context for the today page * @returns The shelf for the given today item, or null if we cant create one */ export function todayShelfForEditorialItemGroup(objectGraph, todayItem, pageContext) { var _a; let isValidHeroStoryGroup = true; const shelf = createTodayShelfWithItems(objectGraph, todayItem, pageContext, () => { var _a, _b, _c, _d, _e, _f, _g; const shelfItems = []; const displayLimit = impressionDemotion.isImpressionDemotionAvailable(objectGraph) ? (_a = asNumber(todayItem.data, "meta.personalizationData.displayEICount")) !== null && _a !== void 0 ? _a : 100 : 100; let groupItems = asArrayOrEmpty(todayItem.data.meta, "associations.recommendations.data"); if (isSome(pageContext.recoImpressionData)) { groupItems = impressionDemotion.personalizeDataItems(groupItems, pageContext.recoImpressionData, (_b = pageContext.pageInformation.recoMetricsData) !== null && _b !== void 0 ? _b : {}); } const groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); let parsedGroupItemCount = 0; for (const [index, groupItem] of groupItems.entries()) { (_c = pageContext.pageInformation.iAdInfo) === null || _c === void 0 ? void 0 : _c.updateContainerId((_d = pageContext.pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.containerIdForSlotIndex((_e = pageContext.parsedCardCount) !== null && _e !== void 0 ? _e : 0)); updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, groupDisplayStyle, parsedGroupItemCount); const cardConfig = createTodayCardConfiguration(objectGraph, groupItem, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), pageContext.currentRowMetricsDisplayStyle, groupDisplayStyle === models.GroupDisplayStyle.Hero && index === 0); const shelfItem = createTodayCardForPageContext(objectGraph, pageContext, cardConfig, groupItem); if (isSome(shelfItem)) { shelfItems.push(shelfItem); nextPosition(pageContext.locationTracker); pageContext.parsedCardCount++; incrementRowIndexIfNecessary(pageContext, groupDisplayStyle, parsedGroupItemCount); parsedGroupItemCount++; } if (cardConfig.isHeroCard && isNothing(shelfItem)) { if (["debug", "internal"].includes(objectGraph.client.buildType)) { validation.unexpectedType("defaultValue", `Hero story group ${(_f = todayItem.data) === null || _f === void 0 ? void 0 : _f.id} must contain a valid hero card at index ${index}. Unable to parse card ${groupItem.id}.`, null); } isValidHeroStoryGroup = false; } if (index < groupItems.length - 1 && pageContext.adPlacementBehavior === models.AdPlacementBehavior.insertIntoShelf) { // Attempt to insert the ad card after creating the next item in the group, but only // if we're still **within** the story group, ads that fall at the beginning or end of // the group are handled at the top level of the page parsing. const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, createTodayCardConfiguration(objectGraph, pageContext.adData, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), undefined, false)); if (isSome(adCard)) { pageContext.parsedCardCount++; incrementRowIndexIfNecessary(pageContext, models.GroupDisplayStyle.Standard, 0); nextPosition(pageContext.locationTracker); shelfItems.push(adCard); } } // if the count is equal to the story limit. stop here and break. if (shelfItems.length === displayLimit) { break; } } if (isValidHeroStoryGroup) { // The number of items required to be a valid hero story group, this includes the hero // card, and two additional cards to form a row const heroStoryGroupRequiredItemCount = 3; if (shelfItems.length !== heroStoryGroupRequiredItemCount) { if (["debug", "internal"].includes(objectGraph.client.buildType)) { validation.unexpectedType("defaultValue", `Hero story group ${(_g = todayItem.data) === null || _g === void 0 ? void 0 : _g.id} must contain exactly ${heroStoryGroupRequiredItemCount} items but only found ${shelfItems.length} items.`, null); } isValidHeroStoryGroup = false; } } return shelfItems; }); // Fallback to standard group display style if we had an issue creating a valid hero group let groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); if (groupDisplayStyle === models.GroupDisplayStyle.Hero && !isValidHeroStoryGroup) { groupDisplayStyle = models.GroupDisplayStyle.Standard; } shelf.contentsMetadata = { type: "todaySection", debugSectionTypeIndicatorColor: color.named("systemGreen"), groupDisplayStyle: groupDisplayStyle, }; // If this is not the first item in the module, attempt to set the background if (!todayItem.isFirstItemInModule) { const editorialShelfBackgroundInfo = todaySectionEditorialBackground(objectGraph, todayItem); if (isSome(editorialShelfBackgroundInfo)) { shelf.background = editorialShelfBackgroundInfo.shelfBackground; if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.configuration)) { shelf.header.configuration.eyebrowColor = editorialShelfBackgroundInfo.eyebrowColor; shelf.header.configuration.titleColor = editorialShelfBackgroundInfo.titleColor; shelf.header.configuration.subtitleColor = editorialShelfBackgroundInfo.subtitleColor; } } else if (groupDisplayStyle === models.GroupDisplayStyle.Hero && Array.isArray(shelf.items)) { shelf.background = todaySectionBackgroundForHeroDisplayStyle(shelf.items); } } return shelf; } /** * @param objectGraph The dependency graph for the App Store * @param todayItem The flattened today item, used to create the items in this shelf * @param pageContext The page context for the today page * @param itemProvider The function that will provide the items for this shelf * @returns The shelf for the given today item */ function createTodayShelfWithItems(objectGraph, todayItem, pageContext, itemProvider) { const shouldRecordShelfMetrics = todayItem.type === FlattenedTodayItemType.EditorialItemGroup; const shelf = new models.Shelf("todayCard"); shelf.id = todayItem.data.id; shelf.isHorizontal = false; shelf.header = createTodayShelfHeaderForTodayItem(objectGraph, todayItem, pageContext); if (shouldRecordShelfMetrics) { const shelfMetricsOptions = { id: shelf.id, kind: "editorialItemGroup", softwareType: null, targetType: "swoosh", title: sectionTitleFromTodayItem(objectGraph, todayItem, true), pageInformation: pageContext.pageInformation, locationTracker: pageContext.locationTracker, idType: "its_id", recoMetricsData: recoMetricsFromTodayItem(todayItem), }; if (todayItem.type === FlattenedTodayItemType.EditorialItemGroup) { shelfMetricsOptions["optimizationId"] = asString(todayItem.data, "meta.personalizationData.optimizationId"); shelfMetricsOptions["optimizationEntityId"] = asString(todayItem.data, "meta.personalizationData.optimizationEntityId"); } addImpressionFields(objectGraph, shelf, shelfMetricsOptions); pushContentLocation(objectGraph, shelfMetricsOptions, shelfMetricsOptions.title); } shelf.items = itemProvider(); shelf.isHidden = isNullOrEmpty(shelf.items); if (shouldRecordShelfMetrics) { popLocation(pageContext.locationTracker); nextPosition(pageContext.locationTracker); } return shelf; } function createTodayShelfHeaderForTodayItem(objectGraph, todayItem, pageContext) { var _a; const shouldSuppressHeader = (_a = asBoolean(todayItem.data, "meta.personalizationData.suppressHeader")) !== null && _a !== void 0 ? _a : false; if (shouldSuppressHeader) { return null; } const shelfHeaderConfiguration = { eyebrowImageColor: null, titleImageColor: null, includeSeparator: false, }; const shelfHeader = { eyebrow: sectionEyebrowFromTodayItem(objectGraph, todayItem), eyebrowArtwork: sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Eyebrow), eyebrowArtworkType: sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Eyebrow), title: sectionTitleFromTodayItem(objectGraph, todayItem), titleArtwork: sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Title), titleArtworkType: sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Title), subtitle: sectionSubtitleFromTodayItem(objectGraph, todayItem), configuration: shelfHeaderConfiguration, }; if (isSome(shelfHeader.eyebrow) || isSome(shelfHeader.title) || isSome(shelfHeader.subtitle)) { return shelfHeader; } else { return null; } } /** * Create a today card for a given today item in a page context. This handles requirements for ad replacement if needed * @param objectGraph The dependency graph of the app store * @param pageContext The context of the page so we can tell whether its time to insert an ad * @param cardConfig The configuration for the today item * @param todayItem The flattened today item that should be placed in the shelf or replaced with an ad card * @returns The today card to insert in a shelf */ function createTodayCardForPageContext(objectGraph, pageContext, cardConfig, todayItem) { var _a, _b; let editorialItemTodayCard; if (pageContext.adPlacementBehavior === models.AdPlacementBehavior.replaceOrganic && isDefinedNonNull(cardConfig.replaceIfAdPresent) && asBoolean(cardConfig.replaceIfAdPresent)) { // the ad card should replace the organic const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, cardConfig); if (isDefinedNonNullNonEmpty(adCard)) { editorialItemTodayCard = adCard; } else { editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); } } else if (pageContext.adPlacementBehavior === models.AdPlacementBehavior.dropAd && isDefinedNonNull(cardConfig.replaceIfAdPresent) && !asBoolean(cardConfig.replaceIfAdPresent)) { // the organic is not replaceable editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); if (isDefinedNonNullNonEmpty(pageContext.adData)) { const recorder = pageContext.adIncidentRecorder; (_a = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "EDITORIALTAKEOVER", (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType); } } else { // we're using existing insertion logic that will insert an ad elsewhere. create a regular card for the flattened item editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); } return editorialItemTodayCard; } // MARK: - Ads /** * Try create a shelf to display an ad if we're currently at the correct spot in the feed. This is only used * for single editorialItem shelves, and the end of an editorial item group, since these locations it does not * make sense to include the ad within the shelf for that TodayItem. * @param objectGraph The dependency graph of the app store * @param pageContext The context of the page so we can tell whether its time to insert an ad * @returns The card to insert at the ad slot if necessary */ export function createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext) { let adShelf = null; if (!isAdPlacementEnabled(objectGraph, "today") || isNothing(pageContext.adData) || pageContext.adPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { return adShelf; } if (pageContext.adLocation !== pageContext.parsedCardCount) { return adShelf; } adShelf = new models.Shelf("todayCard"); adShelf.id = pageContext.adData.id; adShelf.isHorizontal = false; adShelf.contentsMetadata = { type: "todaySection", debugSectionTypeIndicatorColor: color.named("systemBlue"), groupDisplayStyle: models.GroupDisplayStyle.Standard, }; const shelfItems = []; /// Attempt to create an ad card before creating the next item in the group const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, createTodayCardConfiguration(objectGraph, pageContext.adData, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), undefined, false)); if (isSome(adCard)) { pageContext.parsedCardCount++; incrementRowIndexIfNecessary(pageContext, models.GroupDisplayStyle.Standard, 0); nextPosition(pageContext.locationTracker); shelfItems.push(adCard); } adShelf.items = shelfItems; return isDefinedNonNullNonEmpty(adShelf.items) ? adShelf : null; } /** * Try to create an ad card for the page context if the requirements are met * @param objectGraph The dependency graph of the app store * @param pageContext The context of the page so we can tell whether its time to insert an ad * @param cardConfig The configuration of the card * @returns The card to insert at the ad slot if necessary */ export function createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, cardConfig) { var _a, _b, _c; if (!isAdPlacementEnabled(objectGraph, "today")) { return null; } // The ad should respect its slot, unless we're using reco logic for organic replacement. If we need to replace // the organic, we will arbitrarily respect the reco flag to avoid edge cases with mismatching data if (pageContext.adLocation !== pageContext.parsedCardCount && pageContext.adPlacementBehavior !== models.AdPlacementBehavior.replaceOrganic) { return null; } (_a = pageContext.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.updateContainerId((_b = pageContext.pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.containerIdForSlotIndex((_c = pageContext.parsedCardCount) !== null && _c !== void 0 ? _c : 0)); const adCard = createTodayAdCard(objectGraph, pageContext.adData, pageContext.adIncidentRecorder, cardConfig, pageContext); if (isSome(adCard)) { return adCard; } else { return null; } } // MARK: - Shelf Header Content /** * @param objectGraph The dependency graph of the app store * @param item The today item to look for the eyebrow in * @returns The text displayed above the title on a section header */ export function sectionEyebrowFromTodayItem(objectGraph, item) { const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); if (item.isFirstItemInModule || editorialClientParams.suppressHeaderBadge) { return null; } let sectionBadge; const editorialCard = editorialCardFromData(item.data); if (hasAttributes(editorialCard)) { sectionBadge = mediaAttributes.attributeAsString(editorialCard, "headerBadge"); } if (isSome(sectionBadge)) { return sectionBadge; } switch (item.data.type) { case "editorial-items": sectionBadge = attributeAsString(item.data, "headerBadge"); break; case "editorial-item-groups": sectionBadge = attributeAsString(item.data, ["editorialNotes", "badge"]); break; default: break; } return sectionBadge; } /** * @param objectGraph The dependency graph of the app store * @param item The today item to look for the title in * @param alwaysReturnTitle If true, the title will be returned even if it is the first item in a module * @returns The title displayed above a section */ export function sectionTitleFromTodayItem(objectGraph, item, alwaysReturnTitle = false) { const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); if ((item.isFirstItemInModule || editorialClientParams.suppressHeaderName) && !alwaysReturnTitle) { return null; } let sectionTitle; const editorialCard = editorialCardFromData(item.data); if (hasAttributes(editorialCard)) { sectionTitle = mediaAttributes.attributeAsString(editorialCard, "headerName"); } if (isSome(sectionTitle)) { return sectionTitle; } switch (item.data.type) { case "editorial-items": sectionTitle = attributeAsString(item.data, "headerName"); break; case "editorial-item-groups": sectionTitle = attributeAsString(item.data, ["editorialNotes", "name"]); break; default: break; } return sectionTitle; } /** * @param objectGraph The dependency graph of the app store * @param item The today item to look for the subtitle in * @returns The subtitle displayed below the title on a section header */ function sectionSubtitleFromTodayItem(objectGraph, item) { const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); if (item.isFirstItemInModule || editorialClientParams.suppressHeaderTagline) { return null; } let sectionSubtitle; const editorialCard = editorialCardFromData(item.data); if (hasAttributes(editorialCard)) { sectionSubtitle = mediaAttributes.attributeAsString(editorialCard, "headerTagline"); } if (isSome(sectionSubtitle)) { return sectionSubtitle; } switch (item.data.type) { case "editorial-items": sectionSubtitle = attributeAsString(item.data, "headerTagline"); break; case "editorial-item-groups": sectionSubtitle = attributeAsString(item.data, ["editorialNotes", "tagline"]); break; default: break; } return sectionSubtitle; } /** * @param objectGraph The dependency graph of the app store * @param item The today item to look for the subtitle in * @returns The artwork displayed in the eyebrow of a today section. */ function sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, item, placement) { var _a; const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); const headerContents = relationshipData(objectGraph, item.data, "header-contents"); const artworkBehavior = (_a = editorialClientParams.headerArtworkBehavior) !== null && _a !== void 0 ? _a : TodayHeaderArtworkBehavior.NoArtwork; switch (placement) { case TodaySectionHeaderArtworkPlacement.Eyebrow: switch (artworkBehavior) { case TodayHeaderArtworkBehavior.CategoryArtworkWithBadge: return categoryArtworkFromData(objectGraph, headerContents); default: return null; } case TodaySectionHeaderArtworkPlacement.Title: switch (artworkBehavior) { case TodayHeaderArtworkBehavior.CategoryArtworkWithTitle: return categoryArtworkFromData(objectGraph, headerContents); case TodayHeaderArtworkBehavior.ContentArtworkWithTitle: return iconFromData(objectGraph, headerContents, { useCase: 1 /* ArtworkUseCase.LockupIconSmall */, }); default: return null; } default: return null; } } /** * @param objectGraph The dependency graph of the app store * @param item The today item to look for the subtitle in * @returns The artwork type displayed in the eyebrow of a today section. */ function sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, item, placement) { const headerContents = relationshipData(objectGraph, item.data, "header-contents"); const artworkBehavior = attributeAsString(item.data, [ "editorialClientParams", "headerArtworkBehavior", ]); switch (placement) { case TodaySectionHeaderArtworkPlacement.Eyebrow: switch (artworkBehavior) { case TodayHeaderArtworkBehavior.CategoryArtworkWithBadge: const hasCategoryArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); return hasCategoryArtwork ? models.ShelfHeaderArtworkType.Category : null; default: return null; } case TodaySectionHeaderArtworkPlacement.Title: switch (artworkBehavior) { case TodayHeaderArtworkBehavior.CategoryArtworkWithTitle: const hasCategoryArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); return hasCategoryArtwork ? models.ShelfHeaderArtworkType.Category : null; case TodayHeaderArtworkBehavior.ContentArtworkWithTitle: const hasIconArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); return hasIconArtwork ? models.ShelfHeaderArtworkType.Icon : null; default: return null; } default: return null; } } /** * * @param objectGraph The dependency graph of the app store * @param data The MAPI data for the related content for a today item * @returns The artwork to use for the related content */ function categoryArtworkFromData(objectGraph, data) { const artworkData = categoryArtworkData(objectGraph, data, false, false, false); if (isNothing(artworkData)) { return null; } const artwork = artworkFromApiArtwork(objectGraph, artworkData, { useCase: 20 /* ArtworkUseCase.CategoryIcon */, allowingTransparency: true, cropCode: "sr", }); return artwork; } /** * @param objectGraph The dependency graph for the App Store * @param todayItem The today item that contains the data for a editorial-item or editorial-item-group, this is used to look for * editorialBackground, which can then be used to generate the section gradient background * @returns The background to use for the section */ function todaySectionEditorialBackground(objectGraph, todayItem) { const editorialBackground = mediaAttributes.attributeAsDictionary(todayItem.data, "editorialBackground", null); const editorialBackgroundType = editorialBackground === null || editorialBackground === void 0 ? void 0 : editorialBackground["type"]; if (isNothing(editorialBackgroundType)) { return null; } let backgroundInfo = null; switch (editorialBackgroundType) { case EditorialBackgroundType.LinearGradient: const linearGradientData = asInterface(editorialBackground); const colors = linearGradientData.stops.map((stop) => color.fromHex(stop.color)); const shelfBackground = { type: "gradient", colors: colors, start: models.ShelfBackgroundGradientLocation.Top, end: models.ShelfBackgroundGradientLocation.Bottom, }; const isDark = color.isDarkColor(colors[0]); const secondaryLabelLightColor = { type: "rgb", red: 60 / 255, green: 60 / 255, blue: 67 / 255, alpha: 0.6, }; const secondaryLabelDarkColor = { type: "rgb", red: 235.0 / 255, green: 235.0 / 255, blue: 245.0 / 255, alpha: 0.6, }; backgroundInfo = { shelfBackground: shelfBackground, eyebrowColor: isDark ? secondaryLabelDarkColor : secondaryLabelLightColor, titleColor: isDark ? color.named("white") : color.named("black"), subtitleColor: isDark ? secondaryLabelDarkColor : secondaryLabelLightColor, }; break; default: backgroundInfo = null; break; } return backgroundInfo; } /** * @param todayCards The today cards that are contained in the section * @returns The background to use for the section */ function todaySectionBackgroundForHeroDisplayStyle(todayCards) { const backgroundColors = todayCards .map((todayCard) => { return todayCard.media.bestBackgroundColor(); }) .filter((backgroundColor) => isSome(backgroundColor)); let shelfBackground = null; if (backgroundColors.length > 0 && backgroundColors.length <= 4 && backgroundColors.length === todayCards.length) { switch (backgroundColors.length) { case 1: shelfBackground = { type: "materialGradient", colors: { colorCount: "oneColor", color: backgroundColors[0], }, }; break; case 2: shelfBackground = { type: "materialGradient", colors: { colorCount: "twoColor", top: backgroundColors[0], bottom: backgroundColors[1], }, }; break; case 3: shelfBackground = { type: "materialGradient", colors: { colorCount: "threeColor", top: backgroundColors[0], bottomLeading: backgroundColors[1], bottomTrailing: backgroundColors[2], }, }; break; case 4: shelfBackground = { type: "materialGradient", colors: { colorCount: "fourColor", topLeading: backgroundColors[0], topTrailing: backgroundColors[1], bottomLeading: backgroundColors[2], bottomTrailing: backgroundColors[3], }, }; break; default: break; } } else { shelfBackground = { type: "color", color: color.named("secondarySystemBackground"), }; } return shelfBackground; } // MARK: - Metrics /** * Retrieves the recommendation metrics data from a FlattenedTodayItem object. * @param todayItem - The FlattenedTodayItem object containing the module metadata. * @returns The recommendation metrics data as a JSONData object. */ export function recoMetricsFromTodayItem(todayItem) { var _a, _b; if (isNothing(todayItem)) { return {}; } const recoMetricsData = (_a = asDictionary(todayItem.moduleMetadata, "meta.metrics")) !== null && _a !== void 0 ? _a : {}; const combinedRecoMetricsData = (_b = combinedRecoMetricsDataFromMetricsData(recoMetricsData, todayItem.moduleMetadata.onDevicePersonalizationProcessingType, null)) !== null && _b !== void 0 ? _b : {}; return combinedRecoMetricsData; } // MARK: - Debug Util /** * Generate a todayCardPreview url that contains the entire original today feed. * @param objectGraph The dependency graph of the app store * @param todayItems The flattened today feed items * @returns The url to add to the today page for debug purposes */ export function feedPreviewUrlFromFlattenedTodayItems(objectGraph, todayItems) { switch (objectGraph.client.buildType) { case "debug": case "internal": const feedPreviewUrl = new URL(); feedPreviewUrl.protocol = Protocol.https; feedPreviewUrl.host = "apps.apple.com"; feedPreviewUrl.pathname = `/${Path.todayCardPreview}`; const idsParamValues = []; for (const item of todayItems) { switch (item.type) { case FlattenedTodayItemType.EditorialItem: idsParamValues.push(item.data.id); break; case FlattenedTodayItemType.EditorialItemGroup: const groupItems = asArrayOrEmpty(item.data.meta, "associations.recommendations.data"); idsParamValues.push(`${item.data.id}:[${groupItems.map((groupItem) => groupItem.id).join(",")}]`); break; default: break; } } feedPreviewUrl.param(Parameters.ids, idsParamValues.join(",")); feedPreviewUrl.param(Parameters.isTodayFeedPreview, "true"); return decodeURIComponent(feedPreviewUrl.build()); default: return null; } } /** * Generate a todayCardPreview url that can display a single today card * @param objectGraph The dependency graph of the app store * @param todayCardId The id of the created today card * @param cardConfig The card config for the created today card * @returns The url to add to the today page for debug purposes */ export function todayCardPreviewUrlForTodayCard(objectGraph, todayCardId, cardConfig) { if (isNothing(todayCardId) || !objectGraph.client.isiOS) { return null; } switch (objectGraph.client.buildType) { case "debug": case "internal": const feedPreviewUrl = new URL(); feedPreviewUrl.protocol = Protocol.https; feedPreviewUrl.host = "apps.apple.com"; feedPreviewUrl.pathname = `/${Path.todayCardPreview}`; feedPreviewUrl.param(Parameters.ids, `${todayCardId}`); feedPreviewUrl.param(Parameters.isTodayFeedPreview, "true"); feedPreviewUrl.param(Parameters.isTodaySection, cardConfig.useOTDTextStyle ? "true" : "false"); return decodeURIComponent(feedPreviewUrl.build()); default: return null; } } //# sourceMappingURL=today-parse-util.js.map