import { isNothing, isSome, unwrapOptional } from "@jet/environment/types/optional"; import * as models from "../../../api/models"; import * as filtering from "../../filtering"; import * as adLockups from "../../lockups/ad-lockups"; import * as lockups from "../../lockups/lockups"; import * as serverData from "../../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../../foundation/media/attributes"; import * as mediaDataStructure from "../../../foundation/media/data-structure"; import * as mediaRelationships from "../../../foundation/media/relationships"; import { searchLandingPageAdShelfIdentifier, searchLandingPagePositionInfo } from "../../ads/on-device-ad-stitch"; import { createArtworkForResource } from "../../content/artwork/artwork"; import * as content from "../../content/content"; import * as metricsHelpersPage from "../../metrics/helpers/page"; import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; import * as metricsHelpersLocation from "../../metrics/helpers/location"; import { createChartsCategoryShelf } from "../content/search-categories"; import { MediumAdLockupWithScreenshotsBackground, } from "../../../api/models"; import * as adStitch from "../../ads/ad-stitcher"; import * as adIncidents from "../../ads/ad-incident-recorder"; import * as searchShelves from "../content/search-shelves"; import { CollectionShelfDisplayStyle } from "../../editorial-pages/editorial-page-types"; import { buildBrick } from "../../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder"; import { iconFromData } from "../../content/content"; import * as searchHistoryShelf from "../shelves/search-history-shelf"; import { adLogger } from "../search-ads"; import { asString } from "../../../foundation/json-parsing/server-data"; import { addImpressionsFieldsToAd } from "../../metrics/helpers/impressions"; import { makeSearchResultsPageIntent } from "../../../api/intents/search-results-page-intent"; import { getLocale } from "../../locale"; import { actionFor } from "../../../foundation/runtime/action-provider"; import * as impressionDemotion from "../../personalization/on-device-impression-demotion"; import { applySearchAdMissedOpportunityToShelvesIfNeeded } from "../../ads/ad-common"; // #region Shelf Creation export function firstShelfMarkerMatchingUseCase(dataContainer, searchLandingPageContext, onDevicePersonalizationUseCase) { const shelfData = dataContainer.data; if (serverData.isNullOrEmpty(shelfData)) { return null; } for (const dataItem of shelfData) { if (serverData.isNullOrEmpty(dataItem)) { continue; } /// Skip shelf if not valid for context page type. const shelfMetadata = serverData.asDictionary(dataItem, "meta"); const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); if (shelfPageType !== searchLandingPageContext.pageType) { continue; } if (onDevicePersonalizationUseCase === mediaAttributes.attributeAsString(dataItem, "onDevicePersonalizationUseCase")) { return dataItem; } } return null; } /** * Inserts the shelf made from the data container into the grouping parse context * @param objectGraph The App Store dependency graph * @param dataContainer The response data * @param searchLandingPageContext The context for the search page, e.g. landing or focus */ export function insertShelvesIntoSearchPageContext(objectGraph, dataContainer, searchLandingPageContext) { var _a; const shelfData = dataContainer.data; if (serverData.isNullOrEmpty(shelfData)) { return; } // index to compare adPositionInfo let builtShelves = 0; const supportsFocus = objectGraph.bag.mediaAPISearchFocusEnabled && isSome(searchLandingPageContext.pageType); for (const dataItem of shelfData) { if (serverData.isNullOrEmpty(dataItem)) { continue; } /// Skip shelf if not valid for context page type. if (supportsFocus) { const shelfMetadata = serverData.asDictionary(dataItem, "meta"); const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); if (isSome(shelfPageType) && shelfPageType !== searchLandingPageContext.pageType) { continue; } } const adMeta = dataContainer.meta || null; const adUnitShelf = shelfFromAdStitcher(objectGraph, searchLandingPageContext, builtShelves, adMeta); if (isSome(adUnitShelf)) { searchLandingPageContext.shelves.push(adUnitShelf); metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); } /// Get the necessary metadata for the shelf const shelfKind = mediaAttributes.attributeAsString(dataItem, "contentKind"); const shelfAttributes = searchShelves.shelfAttributesFromData(objectGraph, dataItem, shelfKind); const shelfContext = searchLandingPageShelfContext(objectGraph, dataItem, shelfAttributes, searchLandingPageContext, shelfKind); /// Push the shelf content location so each shelf has the correct parent and starting index metricsHelpersLocation.pushContentLocation(objectGraph, shelfContext.metricsOptions, (_a = shelfAttributes.title) !== null && _a !== void 0 ? _a : ""); /// Create the shelf const shelf = createShelf(objectGraph, dataItem, searchLandingPageContext, shelfContext, shelfAttributes, shelfKind); /// Pop the shelf location metricsHelpersLocation.popLocation(searchLandingPageContext.metricsLocationTracker); /// If the shelf is empty, skip it if (serverData.isNullOrEmpty(shelf)) { continue; } /// Add impressions for the shelf metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, [shelf], "searchLanding", shelfContext.metricsOptions.id, searchLandingPageContext.metricsPageInformation); /// Add the shelf to the page context for processing later searchLandingPageContext.shelves.push(shelf); builtShelves += 1; /// Make sure each shelf is represented by its own impressions index position metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); } } function pageTypeFromShelfMetaCategory(category) { switch (category) { case "search-landing": return searchShelves.SearchPageType.Landing; case "search-focus": return searchShelves.SearchPageType.Focus; default: return undefined; } } /** * Creates the appropriate shelf for the shelf's type * @param objectGraph The App Store Object Graph * @param data The data representing this shelf * @param pageContext The context for the shelf's page * @param shelfContext The context for the shelf * @param shelfAttributes The attributes for the shelf * @param shelfKind The content kind for the shelf * @returns A shelf if supported, null otherwise */ function createShelf(objectGraph, data, pageContext, shelfContext, shelfAttributes, shelfKind) { switch (shelfKind) { case models.SearchLandingPageContentKind.Suggestion: if (objectGraph.client.isVision || objectGraph.client.isWeb) { return createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); } else if (pageContext.pageType === searchShelves.SearchPageType.Focus) { return createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); } else { return createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); } case models.SearchLandingPageContentKind.CategoriesAndCharts: return createChartsCategoryShelf(objectGraph, data, false, shelfAttributes, pageContext, shelfContext); case models.SearchLandingPageContentKind.Apps: return createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); case models.SearchLandingPageContentKind.EditorialCollection: if (objectGraph.client.isVision || objectGraph.client.isWeb) { return buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); } else { return null; } default: return createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes); } } /** * Creates the appropriate shelf for the shelf's type * @param objectGraph The App Store Object Graph * @param data The data representing this shelf * @param pageContext The context for the shelf's page * @param shelfContext The context for the shelf * @param shelfAttributes The attributes for the shelf * @returns A shelf if supported, null otherwise */ function createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes) { if (data.type !== "search-recommendations-marker") { return null; } switch (mediaAttributes.attributeAsString(data, "onDevicePersonalizationUseCase")) { case "recentSearches": return searchHistoryShelf.createShelfWithContext(objectGraph, pageContext, shelfAttributes); default: return null; } } /** * Creates the appropriate shelf for the shelf's type * @param objectGraph The App Store Object Graph * @param data The data representing this shelf * @param pageContext The context for the shelf's page */ function createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, data, pageContext) { var _a, _b, _c; const shelf = new models.Shelf("mediumAdLockupWithScreenshotsBackground"); shelf.isHorizontal = false; const offerEnvironment = "dark"; const offerStyle = "white"; const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, targetType: "card", recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), isAdvert: adLockups.isAdvert(objectGraph, data), }); metricsOptions.kind = "adItem"; // Set up iAdInfo metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); const lockupOptions = { offerEnvironment: offerEnvironment, offerStyle: offerStyle, metricsOptions: { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), isAdvert: adLockups.isAdvert(objectGraph, data), disableFastImpressionsForAds: true, }, artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "mediumAdLockupWithScreenshotsBackground"), canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mediumAdLockupWithScreenshotsBackground"), }; const videoConfiguration = { canPlayFullScreen: false, playbackControls: {}, }; let lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); const platformScreenshots = lockup.screenshots[0]; const templateString = adLockups.getTemplateTypeForMediumAdFromLockupWithScreenshots(platformScreenshots); pageContext.metricsPageInformation.iAdInfo.setTemplateType(templateString); const iconData = iconFromData(objectGraph, data, { useCase: 0 /* ArtworkUseCase.Default */, withJoeColorPlaceholder: true, overrideTextColorKey: "textColor2", }); // Update the lockup value after setting the template type so that the value gets added to the lockup. lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); if (objectGraph.props.enabled("advertSlotReporting")) { (_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setTemplateType(templateString); } else { (_b = lockup.searchAd) === null || _b === void 0 ? void 0 : _b.setTemplateType(templateString); } const backgroundColor = iconData.backgroundColor; const secondaryTextColor = iconData.textColor; const mediumAd = new MediumAdLockupWithScreenshotsBackground(lockup, [platformScreenshots], true, secondaryTextColor, backgroundColor, (_c = objectGraph.bag.todayAdMediumLockupScreenshotsRiverSpeed) !== null && _c !== void 0 ? _c : 8); addImpressionsFieldsToAd(objectGraph, mediumAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); mediumAd.clickAction = lockups.actionFromData(objectGraph, data, metricsOptions, null); shelf.items = [mediumAd]; return shelf; } /** * Creates the appropriate shelf for the shelf's type * @param objectGraph The App Store Object Graph * @param data The data representing this shelf * @param pageContext The context for the shelf's page */ function createCondensedAdLockupWithIconBackgroundShelf(objectGraph, data, pageContext) { var _a, _b, _c; const shelf = new models.Shelf("condensedAdLockupWithIconBackground"); shelf.isHorizontal = false; const offerEnvironment = "dark"; const offerStyle = "white"; const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, targetType: "card", recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), isAdvert: adLockups.isAdvert(objectGraph, data), }); metricsOptions.kind = "adItem"; // Set up iAdInfo metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); const lockupOptions = { offerEnvironment: offerEnvironment, offerStyle: offerStyle, metricsOptions: { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), isAdvert: adLockups.isAdvert(objectGraph, data), disableFastImpressionsForAds: true, }, artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "condensedAdLockupWithIconBackground"), canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "condensedAdLockupWithIconBackground"), }; (_a = pageContext.metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType("APPLOCKUP"); const videoConfiguration = { canPlayFullScreen: false, playbackControls: {}, }; const lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); if (objectGraph.props.enabled("advertSlotReporting")) { (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType("APPLOCKUP"); } else { (_c = lockup.searchAd) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); } const condensedAd = new models.CondensedAdLockupWithIconBackground(lockup, lockup.icon); addImpressionsFieldsToAd(objectGraph, condensedAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); shelf.items = [condensedAd]; return shelf; } // #endregion // #region Suggested Links/Actions Shelves /** * Creates the suggested links shelf * @param objectGraph The App Store dependency graph * @param data The content items for the shelf * @param pageContext The context for the shelf's page * @param shelfAttributes The attributes for the shelf and its contents * @returns A shelf for the suggested links, or null if the shelf would be empty */ function createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { var _a; const linkData = mediaRelationships.relationshipCollection(data, "contents"); const items = []; const shelf = new models.Shelf("action"); shelf.isHorizontal = false; shelf.title = shelfAttributes.title; shelf.presentationHints = { isWidthConstrained: true }; for (const [linkIndex, link] of linkData.entries()) { const metricsBase = { targetType: "link", pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, }; const searchAdAction = trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes); if (serverData.isNull(searchAdAction) || serverData.isNull(searchAdAction.action)) { continue; } metricsHelpersImpressions.addImpressionFields(objectGraph, searchAdAction.action, { ...metricsBase, kind: "link", softwareType: null, title: searchAdAction.action.title, id: `${linkIndex}`, idType: "sequential", }); items.push(searchAdAction); metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); } if (serverData.isNullOrEmpty(items)) { return null; } shelf.items = items; if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { shelf.contentsMetadata = { type: "searchLandingTrendingSection", numberOfColumns: shelfAttributes.displayStyle.layoutSize, }; } else if (objectGraph.client.isPhone || objectGraph.client.isPad) { shelf.contentsMetadata = { type: "searchLandingTrendingSection", numberOfColumns: items.length >= 6 ? 2 : 1, }; } return shelf; } /** * Creates the suggested searches shelf for the focus page. * @param objectGraph The App Store dependency graph * @param data The content items for the shelf * @param pageContext The context for the shelf's page * @param shelfAttributes The attributes for the shelf and its contents * @returns A shelf for the suggested links, or null if the shelf would be empty */ function createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { var _a, _b, _c; const linkData = mediaRelationships.relationshipCollection(data, "contents"); if (isNothing(linkData)) { return null; } const items = []; const shelf = new models.Shelf("singleColumnList"); shelf.isHorizontal = false; shelf.title = shelfAttributes.title; shelf.presentationHints = { isWidthConstrained: true }; for (const [linkIndex, link] of linkData.entries()) { const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); if (isNothing(searchTerm) || searchTerm.length === 0) { continue; // search term is required } const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; const searchAction = createFocusPageSearchAction(objectGraph, displayTerm !== null && displayTerm !== void 0 ? displayTerm : "", searchTerm !== null && searchTerm !== void 0 ? searchTerm : "", undefined, pageContext.metricsLocationTracker, "suggested", undefined, /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. pageContext.metricsPageInformation, (_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined); if (isNothing(searchAction) || serverData.isNullOrEmpty(searchAction)) { continue; } metricsHelpersImpressions.addImpressionFields(objectGraph, searchAction, { targetType: "link", pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, kind: "link", softwareType: null, title: (_c = searchAction.title) !== null && _c !== void 0 ? _c : "", id: `${linkIndex}`, idType: "sequential", }); items.push(searchAction); metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); } if (serverData.isNullOrEmpty(items)) { return null; } shelf.items = items; return shelf; } /** * Creates the suggested links shelf * @param objectGraph The App Store dependency graph * @param data The content items for the shelf * @param pageContext The context for the shelf's page * @param shelfAttributes The attributes for the shelf and its contents * @returns A shelf for the suggested links, or null if the shelf would be empty */ function createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { var _a; const linkData = mediaRelationships.relationshipCollection(data, "contents"); const links = []; const shelf = new models.Shelf("searchLink"); shelf.isHorizontal = false; shelf.title = shelfAttributes.title; shelf.presentationHints = { isWidthConstrained: true }; metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); for (const [linkIndex, link] of linkData.entries()) { const metricsBase = { targetType: "link", pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, }; const searchLink = trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes); metricsHelpersImpressions.addImpressionFields(objectGraph, searchLink, { ...metricsBase, kind: "link", softwareType: null, title: searchLink.clickAction.title, id: `${linkIndex}`, idType: "sequential", }); if (serverData.isNullOrEmpty(searchLink)) { continue; } links.push(searchLink); metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); } if (serverData.isNullOrEmpty(links)) { return null; } shelf.items = links; if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { shelf.contentsMetadata = { type: "searchLandingTrendingSection", numberOfColumns: shelfAttributes.displayStyle.layoutSize, }; } else if (objectGraph.client.isPhone) { shelf.contentsMetadata = { type: "searchLandingTrendingSection", numberOfColumns: links.length >= 6 ? 2 : 1, }; } return shelf; } /** * Creates a suggestion link action for the link data * NOTE: This is legacy and the newer SLP protocol uses `trendingSearchLinkFromData` * @param objectGraph The App Store dependency graph * @param link The data for an individual suggestion link * @param pageContext The page context for the search link * @param shelfAttributes The attributes for the shelf and its contents * @returns An action representing the suggestion link */ function trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes) { var _a, _b; /// We should always have search term, but not necessarily displayTerm. const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); if (isNothing(searchTerm) || searchTerm.length === 0) { return null; } const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. const searchAction = new models.SearchAction(displayTerm, searchTerm, null, "suggested", undefined, undefined); searchAction.artwork = createArtworkForSearchAction((_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined, objectGraph); metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); return isSome(searchAction) ? new models.SearchAdAction(searchAction) : null; } /** * Creates a search action * @param objectGraph The App Store dependency graph * @param searchTerm The term to search for * @param entity The entity to scope the search to, e.g. apps, stories, arcade * @param metricsLocationTracker The metrics location tracker * @param origin The source the search was fired from * @param isFocusPage Whether the origin is in context of the focus page * @param displayStyle The style to display the action item * @returns An action representing a search */ export function createFocusPageSearchAction(objectGraph, title, searchTerm, entity, metricsLocationTracker, origin, source, metricsPageInformation = undefined, displayStyle) { if (serverData.isNullOrEmpty(searchTerm)) { return null; } // For Search Focus Page, text uses primary color and icon uses secondary color (instead of tintColor). const searchAction = new models.SearchAction(title, searchTerm, null, origin, entity !== null && entity !== void 0 ? entity : undefined, source, []); searchAction.artwork = createArtworkForSearchAction(displayStyle, objectGraph); metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", metricsLocationTracker, metricsPageInformation); return searchAction; } function createArtworkForSearchAction(displayStyle, objectGraph) { var _a; if ((displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === models.SearchLandingPageShelfItemIconKind.Symbol && ((_a = displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === null || _a === void 0 ? void 0 : _a.length)) { return createArtworkForResource(objectGraph, `systemimage://${displayStyle.iconSymbol}`); } else if (objectGraph.client.isPhone || objectGraph.client.isVision) { return createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); } return undefined; } /** * Creates a suggestion link action for the link data * @param objectGraph The App Store dependency graph * @param link The data for an individual suggestion link * @param pageContext The page context for the search link * @param shelfAttributes The attributes for the shelf and its contents * @returns An action representing the suggestion link */ function trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes) { var _a, _b, _c; const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); if (isNothing(searchTerm) || searchTerm.length === 0) { return null; } const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; /// we should always have search term, but not necessarily displayTerm let searchAction; if (objectGraph.client.isWeb) { const intent = makeSearchResultsPageIntent({ ...getLocale(objectGraph), origin: "suggested", term: displayTerm, platform: (_b = objectGraph.activeIntent) === null || _b === void 0 ? void 0 : _b.previewPlatform, }); searchAction = unwrapOptional(actionFor(intent, objectGraph)); } else { searchAction = new models.SearchAction(displayTerm, displayTerm, null, "suggested"); metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); } const artwork = createArtworkForSearchAction((_c = shelfAttributes.searchLandingItemDisplayStyle) !== null && _c !== void 0 ? _c : undefined, objectGraph); return new models.SearchLink(displayTerm, searchAction, artwork, null); } // #endregion // #region Discover Lockups Shelves /** * Creates a shelf composed of lockups * @param objectGraph The App Store Object Graph * @param data The data representing this lockups shelf * @param pageContext The page context for the shelf * @param shelfAttributes The attributes for the shelf * @param shelfContext The context for the shelf * @returns A lockups shelf if any lockups could be made */ function createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { var _a, _b, _c, _d, _e; const filterType = 80894 /* filtering.Filter.All */; const items = []; let hasAdLockup = false; let shelfData = initialShelfContentsFromData(data); // Set metadata const shelf = new models.Shelf(shelfContext.shelfStyle); shelf.isHorizontal = false; shelf.title = shelfAttributes.title; if (objectGraph.client.isVision) { shelf.shouldFilterApps = !mediaAttributes.attributeAsBooleanOrFalse(data, "doNotFilter"); } else { shelf.shouldFilterApps = false; } shelf.filteringExcludedItems = shelfContext.filteringExcludedItems; // Stitch First Position Ad (only if lockup array is nonempty) if (serverData.isDefinedNonNullNonEmpty(shelfData)) { const adLockup = lockupFromAdStitcher(objectGraph, pageContext, shelfContext); if (adLockup && adLockup instanceof models.Lockup) { hasAdLockup = true; items.push(adLockup); metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); shelfData = shelfData.filter((shelfItem) => shelfItem.id !== adLockup.adamId); // Filter dupe } } // Determines whether all apps are displayed on SLP suggested apps shelf. const hasDisplayCount = isSome(shelfAttributes.displayCount); // If on device personalization is available, we need to personalize the data items. This reorders the shelf. if (serverData.isDefinedNonNullNonEmpty(shelfData)) { shelfData = impressionDemotion.personalizeDataItems(shelfData, (_a = pageContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_c = (_b = shelfContext.metricsOptions) === null || _b === void 0 ? void 0 : _b.recoMetricsData) !== null && _c !== void 0 ? _c : {}); } // Build Lockups for (const lockupData of shelfData) { // If we encounter a type of app-events, this means they have been incorrectly programmed, // and we should throw the shelf away. if (lockupData.type === "app-events") { return null; } if (serverData.isNull(lockupData.attributes)) { continue; } // Filter out unwanted content if (filtering.shouldFilter(objectGraph, lockupData, filterType)) { continue; } const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); if (lockup) { items.push(lockup); metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); } } if (hasDisplayCount) { // number of apps displayed on SLP suggested shelf const displayCount = shelfAttributes.displayCount; shelf.items = items.slice(0, displayCount); } else { shelf.items = items; } if (hasDisplayCount) { const seeAllShelf = new models.Shelf(shelfContext.shelfStyle); if (hasAdLockup) { // Ad is the first item in array so we are dropping it here. seeAllShelf.items = items.splice(1, items.length - 1); } else { seeAllShelf.items = items; } // Setup Page const seeAllPage = new models.GenericPage([seeAllShelf]); seeAllPage.title = shelf.title; // Setup action const seeAllAction = new models.FlowAction("page"); seeAllAction.pageUrl = shelfAttributes.seeAllLink; seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); seeAllAction.pageData = seeAllPage; // Connect action shelf.seeAllAction = seeAllAction; // Metrics metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, seeAllAction.pageUrl, { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, }); const seeAllPageInformation = metricsHelpersPage.pageInformationForRoom(objectGraph, data.id); metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, seeAllPage, seeAllPageInformation); } // Honor A/B treatment for using horizontal shelf for suggested apps, which may have an optional rowsPerColumn defined. // NOTE: Client will choose an appropriate `rowsPerColumn` at display time, if not provided by server. if (((_d = shelfAttributes.displayStyle) === null || _d === void 0 ? void 0 : _d.layout) === "horizontal" /* models.GenericSearchPageShelfDisplayStyleLayout.Horizontal */) { shelf.isHorizontal = true; shelf.rowsPerColumn = (_e = shelfAttributes.displayStyle) === null || _e === void 0 ? void 0 : _e.layoutSize; } return shelf; } /** * Create a lockup for shelfContents to display within a grouping shelf * @param objectGraph * @param lockupData shelfContents to create lockup for. * @param pageContext The page context for the lockup * @param shelfContext The shelf context for the lockup */ function lockupFromData(objectGraph, lockupData, pageContext, shelfContext) { if (serverData.isNullOrEmpty(lockupData)) { return null; } if (shelfContext.shelfStyle !== "smallLockup") { return null; } let offerStyle = null; if (serverData.isDefinedNonNull(shelfContext.shelfBackground) && (shelfContext.shelfBackground.type === "color" || shelfContext.shelfBackground.type === "interactive")) { offerStyle = "white"; } // Create the lockup const lockupOptions = { metricsOptions: { pageInformation: pageContext.metricsPageInformation, locationTracker: pageContext.metricsLocationTracker, recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(lockupData), isAdvert: adLockups.isAdvert(objectGraph, lockupData), }, artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfContext.shelfStyle), offerStyle: offerStyle, canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfContext.shelfStyle), isContainedInPreorderExclusiveShelf: false, shouldHideArcadeHeader: false, }; const lockup = lockups.lockupFromData(objectGraph, lockupData, lockupOptions); if (serverData.isNull(lockup) || !lockup.isValid()) { return null; } return lockup; } /** * Performs `lockupFromData`, but additional with Ad stitch related side-effects. * @param objectGraph The AppStore dependency graph * @param pageContext The page context for the ad lockup * @param shelfContext The shelf context for the ad lockup */ function lockupFromAdStitcher(objectGraph, pageContext, shelfContext) { const task = adStitch.consumeTask(pageContext.adStitcher, shelfContext.adPositionInfo); if (serverData.isNull(task)) { return null; // no task for position } // Try to create lockup const lockupData = task.data; try { const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); if (serverData.isDefinedNonNull(lockup)) { shelfContext.filteringExcludedItems = [lockupData.id]; } else { adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); } return lockup; } catch (error) { adLogger(objectGraph, `Failed to create SLP ad lockup: ${error}`); adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); return null; } } // #endregion // #region Brick Shelves function buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { const items = []; const shelf = new models.Shelf("brick"); shelf.isHorizontal = mediaAttributes.attributeAsString(data, "layoutDirection") === "Horizontal"; const shelfContents = mediaRelationships.relationshipCollection(data, "contents"); for (const itemData of shelfContents) { const metricsOptions = { ...shelfContext.metricsOptions, targetType: "brickMedium", recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), }; // Using the location tracker from the context will cause positions to be unintentionally incremented // by the lockup builder. Since we're only extracting icons we won't use the lockup metrics, so we can // pass in a new, unused, location tracker instead. const lockupMetricsOptions = { pageInformation: metricsOptions.pageInformation, locationTracker: metricsHelpersLocation.newLocationTracker(), }; const brick = buildBrick(objectGraph, itemData, CollectionShelfDisplayStyle.BrickMedium, metricsOptions, lockupMetricsOptions); brick.clickAction = createPrimaryActionForComponentFromData(objectGraph, itemData, shelfContext); if (!brick.isValid()) { continue; } items.push(brick); metricsHelpersLocation.nextPosition(shelfContext.metricsOptions.locationTracker); } shelf.title = shelfAttributes.title; shelf.items = items; return shelf; } export function createPrimaryActionForComponentFromData(objectGraph, data, shelfContext) { const clickOptions = createBrickClickOptionsFromData(objectGraph, data, shelfContext); const primaryAction = lockups.actionFromData(objectGraph, data, clickOptions, null); return primaryAction; } function createBrickClickOptionsFromData(objectGraph, data, shelfContext) { const clickOptions = { ...shelfContext.metricsOptions, id: data.id, targetType: "brickMedium", }; return clickOptions; } // #endregion // #region Shelf Data Extraction /** * Gets the initial raw shelf contents from the MAPI data object * @param mediaApiData The raw MAPI data * @returns A collection of data objects representing a shelf's contents */ function initialShelfContentsFromData(mediaApiData) { const shelfContents = mediaRelationships.relationship(mediaApiData, "contents"); return shelfContents === null || shelfContents === void 0 ? void 0 : shelfContents.data; } // #endregion // #region Context Generators /** * Performs either `createMediumAdLockupWithScreenshotsBackgroundShelf` or `createCondensedAdLockupWithIconBackgroundShelf * depending on the format defined in adDisplayStyle, but with Ad stitch related side-effects. * @param objectGraph The AppStore dependency graph * @param pageContext The page context for the ad lockup * @param builtShelves The index of the current shelf * @param adMeta The SearchLandingPageAdMeta which includes adDisplayStyle */ function shelfFromAdStitcher(objectGraph, pageContext, builtShelves, adMeta) { var _a; const task = adStitch.consumeTask(pageContext.adStitcher, { shelfIdentifier: searchLandingPageAdShelfIdentifier, slot: builtShelves, }); if (serverData.isNull(task)) { return null; // no task for position } const adData = task.data; try { switch ((_a = adMeta === null || adMeta === void 0 ? void 0 : adMeta.adDisplayStyle) === null || _a === void 0 ? void 0 : _a.format) { case "medium": return createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, adData, pageContext); case "condensed": return createCondensedAdLockupWithIconBackgroundShelf(objectGraph, adData, pageContext); default: adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); return null; } } catch (error) { adLogger(objectGraph, `Failed to create SLP ad shelf: ${error}`); adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); return null; } } /** * Generates a shelf context for a search landing page shelf * @param objectGraph The App Store Object Graph * @param data The shelf's data object * @param shelfAttributes The shelf's attribtues * @param pageContext The context for the page containing the shelf * @param shelfContentKind The type of content the shelf contains * @returns A shelf context for a search landing page shelf */ function searchLandingPageShelfContext(objectGraph, data, shelfAttributes, pageContext, shelfContentKind = null) { const baseShelfContext = searchShelves.baseShelfContext(objectGraph, data, shelfAttributes, pageContext); switch (shelfContentKind) { case models.SearchLandingPageContentKind.Apps: return { ...baseShelfContext, shelfStyle: "smallLockup", adPositionInfo: searchLandingPagePositionInfo, }; default: return baseShelfContext; } } // #endregion //# sourceMappingURL=search-landing-shelf-controller.js.map