diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js | 1047 |
1 files changed, 1047 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js new file mode 100644 index 0000000..a1e27b0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js @@ -0,0 +1,1047 @@ +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
\ No newline at end of file |
