From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../common/search/landing/search-landing-cohort.js | 84 +++ .../landing/search-landing-shelf-controller.js | 836 +++++++++++++++++++++ 2 files changed, 920 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/search/landing') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js new file mode 100644 index 0000000..6baf947 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js @@ -0,0 +1,84 @@ +/** + * Manages storing Cohort IDs for Search Landing Page (SLP) + * + * # What is Cohort ID? + * Cohort IDs (a.k.a. cluster IDs) are used to bucket users into different categories, e.g. a gamer. + * This ID can be used to specify a SLP that is suited to that cateogry of users, e.g. a page featuring more games. + * + * # SLP Endpoint Constraints + * Today, SLPs are not personalized, and rely heavily on CDN caching. + * We cannot fire a single request to SLP endpoint to have it adapt to a user's cohort based on cookies, etc. + * + * As a workaround we: + * 1. Store the cohort ID for user if we ever load a personalized endpoint, e.g. Today. + * 2. Send stored cohort ID as a query param on the SLP endpoint, if we have any. + */ +"use strict"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +// region exports +/** + * Should be called whenever we recieve a MAPI response from a personalized endpoint. + * Persists the cohort id that may be present in given MAPI respose for specified user. + * @param dsid The user's dsid. + * @param metaDataProviding MAPI response that may contain cohort id for current user. + */ +export function storeCohortIdForUserFromResponse(objectGraph, dsid, metaDataProviding) { + if (serverData.isNullOrEmpty(dsid)) { + return; + } + const cohortId = cohortIdFromResponse(metaDataProviding); + if (serverData.isNullOrEmpty(cohortId)) { + return; + } + setCohortIdForDSID(objectGraph, dsid, cohortId); +} +/** + * Return the stored cohort id for given dsid. + * @param dsid The DSID of user to fetch cohort id for. + */ +export function cohortIdForUser(objectGraph, dsid) { + if (serverData.isNullOrEmpty(dsid)) { + return null; + } + return getCohortIdForDSID(objectGraph, dsid); +} +/** + * Deletes cohort id for given user. For testing. + */ +export function deleteCohortIdForUser(objectGraph, dsid) { + setCohortIdForDSID(objectGraph, dsid, undefined); +} +// endregion +// region Internals +/** + * Given a top-level MAPI response `metaDataProviding`, returns the cohort ID, if any. + * @param metaDataProviding A MAPI Response that may contain a `meta.metrics` blob with `clusterId` + */ +function cohortIdFromResponse(metaDataProviding) { + return serverData.asString(metaDataProviding, "meta.metrics.clusterId"); +} +/** + * Converts a DSID into a dictionary key for storing cohort ID. + * @param dsid DSID to generate storage key for. + */ +function cohortIDStorageKeyForDSID(dsid) { + return dsid + "-cohort-id"; +} +/** + * Set the stored cohort id for given user (by dsid). + * @param dsid DSID to associate cohortId with + * @param cohortId The cohort id for user. + */ +function setCohortIdForDSID(objectGraph, dsid, cohortId) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + objectGraph.storage.storeString(cohortForDSIDKey, cohortId); +} +/** + * Gets the stored cohort id for given user by dsid. + * @param dsid DSID to get cohort id for. + */ +function getCohortIdForDSID(objectGraph, dsid) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + return objectGraph.storage.retrieveString(cohortForDSIDKey); +} +//# sourceMappingURL=search-landing-cohort.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js new file mode 100644 index 0000000..808384b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js @@ -0,0 +1,836 @@ +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 \ No newline at end of file -- cgit v1.2.3