diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/today/article.js | |
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/today/article.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/today/article.js | 1572 |
1 files changed, 1572 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/article.js b/node_modules/@jet-app/app-store/tmp/src/common/today/article.js new file mode 100644 index 0000000..a67fde4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/article.js @@ -0,0 +1,1572 @@ +/** + * Created by keithpk on 3/21/17. + */ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaAugment from "../../foundation/media/augment"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationships from "../../foundation/media/relationships"; +import * as urls from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import { PageID } from "../../gameservicesui/src/common/id-builder"; +import * as gamesComponentBuilder from "../../gameservicesui/src/editorial-page/editorial-component-builder"; +import * as appPromotionsShelf from "../app-promotions/app-promotions-shelf"; +import * as arcadeCommon from "../arcade/arcade-common"; +import * as arcadeUpsell from "../arcade/arcade-upsell"; +import * as breakoutsCommon from "../arcade/breakouts-common"; +import * as videoDefaults from "../constants/video-constants"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as content from "../content/content"; +import { EditorialMediaPlacement } from "../editorial-pages/editorial-media-util"; +import { buildSmallStoryCardShelf } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder"; +import { buildStoryCard } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils"; +import { createBaseShelfToken } from "../editorial-pages/editorial-page-shelf-token"; +import { CollectionShelfDisplayStyle } from "../editorial-pages/editorial-page-types"; +import * as externalDeepLink from "../linking/external-deep-link"; +import * as links from "../linking/os-update-links"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMedia from "../metrics/helpers/media"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as sharing from "../sharing"; +import { crossLinkSubtitleFromData, defaultTodayCardConfiguration, fallbackWatchTodayCardFromData, todayCardFromData, } from "./today-card-util"; +import * as todayHorizontalCardUtil from "./today-horizontal-card-util"; +import { todayCardPreviewUrlForTodayCard } from "./today-parse-util"; +import { HeroMediaDisplayContext, TodayCardDisplayStyle, TodayParseContext, } from "./today-types"; +export const iAPBackgroundColor = color.named("componentBackgroundStandout"); +const appShowcaseBackgroundColor = color.named("componentBackgroundStandout"); +const arcadeShowcaseShelfBackgroundColor = color.named("componentBackgroundStandout"); +/** + * Resolves the article module's app media platform to an `AppPlatform` to use for screenshots. + * @param {AppMediaPlatform} appMediaPlatform The server-dictated media platform to use for the module. + * @returns {AppPlatform} The app platform that is appropriate for this media platform, taking into account our device. + */ +function appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform) { + switch (appMediaPlatform) { + case "Watch": + return "watch"; + case "iOS": + if (objectGraph.client.isPad) { + return "pad"; + } + else { + return "phone"; + } + case "tvOS": + return "tv"; + case "Messages": + return "messages"; + case "visionOS": + return "vision"; + default: + return null; + } +} +export class ArticleParseContext { + constructor() { + // The index of the current module + this.index = 0; + // The reco metrics from the shelf on the today page + this.todayShelfRecoMetricsData = {}; + /// Whether there are any focusable elements (for touch mode) + this.hasFocusableElements = false; + /// Whether there are any non-focusable elements (for touch mode) + this.hasNonFocusableElements = false; + /// Whether there is a resilient deep link. + this.isResilientDeepLink = false; + /// Whether or not to allow app event previews, used by editorial to preview app event stories before they are published + this.allowUnpublishedAppEventPreviews = false; + } +} +function todayCardConfigFromArticleContext(objectGraph, articleContext) { + if (!serverData.isDefinedNonNull(articleContext)) { + return null; + } + if (isSome(articleContext.todayCardConfig)) { + return articleContext.todayCardConfig; + } + const config = defaultTodayCardConfiguration(objectGraph); + config.enableListCardToMultiAppFallback = false; + config.clientIdentifierOverride = articleContext.clientIdentifierOverride; + config.useOTDTextStyle = false; + config.allowUnpublishedAppEventPreviews = articleContext.allowUnpublishedAppEventPreviews; + config.currentRowIndex = undefined; + switch (objectGraph.client.deviceType) { + case "mac": + config.prevailingCropCodes = { defaultCrop: "en" }; + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + config.heroDisplayContext = HeroMediaDisplayContext.Article; + break; + case "tv": + config.prevailingCropCodes = { + "defaultCrop": "ek", + "editorialArtwork.storyCenteredStatic16x9": "SCS.ApDHXL01", + }; + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + config.heroDisplayContext = HeroMediaDisplayContext.Article; + break; + case "web": + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.River; + config.prevailingCropCodes = { + "defaultCrop": "sr", + "editorialArtwork.dayCard": "grav.west", + }; + break; + default: + break; + } + return config; +} +export function articlePageFromResponse(objectGraph, articleResponse, context) { + return validation.context("articlePageWithResponse", () => { + var _a; + const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse); + context.metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "editorialItem", articleData.id, articleResponse); + context.metricsLocationTracker = metricsHelpersLocation.newLocationTracker(); + context.pageId = articleData.id; + // Bridge over article contexts to today's metrics context and card config + const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker, context.refreshController); + const todayCardConfig = todayCardConfigFromArticleContext(objectGraph, context); + // Render the top card + let todayCard = todayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext); + let editorialStoryCard = null; + const todayCardMedia = todayCard === null || todayCard === void 0 ? void 0 : todayCard.media; + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + editorialStoryCard = buildStoryCard(objectGraph, articleData, EditorialMediaPlacement.StoryDetail, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, CollectionShelfDisplayStyle.StoryMedium, false); + todayCard = null; + } + if (isNothing(todayCard)) { + todayCard = fallbackWatchTodayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext); + } + // Get the title for metrics purposes. + const title = (_a = todayCard === null || todayCard === void 0 ? void 0 : todayCard.title) !== null && _a !== void 0 ? _a : editorialStoryCard === null || editorialStoryCard === void 0 ? void 0 : editorialStoryCard.title; + const editorialItemKind = mediaAttributes.attributeAsString(articleData, "kind"); + // Configure subtitle for cross link + context.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, articleData); + // Bridge today config back into articles, now that cards are created. + // Now we've created the card, reference the clientIdentifierOverride it used for the rest of the article. + context.clientIdentifierOverride = todayCardConfig.clientIdentifierOverride; + // Start a metrics location + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + targetType: "article", + id: context.pageId, + idType: "its_id", + }, title); + // Render the article itself + const shelves = renderArticle(objectGraph, articleData, todayCardMedia, context); + const lastShelf = shelves[shelves.length - 1]; + // Sharing + const shareAction = objectGraph.client.isTV || + objectGraph.client.isWeb || + context.isResilientDeepLink || + preprocessor.GAMES_TARGET || + editorialItemKind === "OfferItem" + ? null + : shareSheetActionFromData(objectGraph, articleData, todayCardConfig); + if (serverData.isDefinedNonNull(shareAction)) { + // Add click event + metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { + targetType: "button", + id: context.pageId, + actionType: "share", + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }); + const isLastModuleFullWidth = isArticleShelfFullWidth(objectGraph, lastShelf, context.module); + const shareButtonShelf = createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth); + if (shareButtonShelf) { + shelves.push(shareButtonShelf); + } + } + const page = new models.ArticlePage(todayCard, shelves, shareAction); + page.editorialStoryCard = editorialStoryCard; + page.title = todayCard === null || todayCard === void 0 ? void 0 : todayCard.title; + page.subtitle = todayCard === null || todayCard === void 0 ? void 0 : todayCard.inlineDescription; + addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context); + if (objectGraph.client.isTV) { + if (context.hasFocusableElements && !context.hasNonFocusableElements) { + page.touchMode = "focus"; + } + else if (!context.hasFocusableElements && context.hasNonFocusableElements) { + page.touchMode = "pan"; + } + else { + page.touchMode = "auto"; + } + } + // Map whether the article should terminate on close. + page.shouldTerminateOnClose = context.isResilientDeepLink; + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, context.metricsPageInformation, (fields) => { + let additionalValue = title; + if ((todayCard === null || todayCard === void 0 ? void 0 : todayCard.media) instanceof models.TodayCardMediaBrandedSingleApp && + (todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay) instanceof models.TodayCardLockupOverlay) { + const lockupOverlay = todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay; + additionalValue = lockupOverlay.lockup.title; + } + if (!additionalValue) { + return; + } + let pageDetails = serverData.asString(serverData.asJSONValue(fields["pageDetails"]), "coercible"); + pageDetails = pageDetails || serverData.asString(serverData.asJSONValue(fields["pageId"])); + if (pageDetails) { + fields["pageDetails"] = `${pageDetails}_${additionalValue}`; + } + else { + fields["pageDetails"] = `unknown_${additionalValue}`; + } + }); + page.canonicalURL = mediaAttributes.attributeAsString(articleData, "url"); + if (isSome(articleData)) { + const articleUrl = mediaAttributes.attributeAsString(articleData, "url"); + if (isSome(articleUrl)) { + page.viewArticleAction = new models.ExternalUrlAction(articleUrl, true); + } + } + return page; + }); +} +function renderArticle(objectGraph, articleData, cardMedia, context) { + return validation.context("renderArticle", () => { + var _a; + const shelves = []; + const canvas = (_a = mediaRelationships.relationshipCollection(articleData, "canvas")) !== null && _a !== void 0 ? _a : []; + for (const storyModule of canvas) { + context.module = mediaAttributes.attributeAsString(storyModule, "displayType"); + context.subStyle = null; + const shelfIndex = shelves.length; + const shelvesToRender = renderModule(objectGraph, storyModule, articleData, context, shelfIndex); + if (shelvesToRender.length > 0) { + for (const shelf of shelvesToRender) { + shelf.title = context.titleForNextShelf; + if (objectGraph.client.isTV) { + // Skip unsupported tvOS shelves + if (shelf.contentType === "editorialLink") { + continue; + } + } + else if (objectGraph.client.isWatch) { + // Skip unsupported watchOS shelves + if (shelf.contentType === "editorialLink") { + continue; + } + } + shelves.push(shelf); + context.titleForNextShelf = null; + } + } + context.index++; + metricsHelpersLocation.nextPosition(context.metricsLocationTracker); + } + // If we we're showing the fallback list card type on the today page, we're going to show + // the lockups as a list shelf underneath so we can still display the list contents. + // If we're on watchOS, we also want to hit this codepath so that lockup lists do not + // show as empty pages. + if ((context.showingFallbackMediaInline || + objectGraph.client.isWatch || + objectGraph.client.isVision || + objectGraph.client.isWeb || + preprocessor.GAMES_TARGET) && + shelves.length === 0) { + const fallbackShelf = createFallbackListShelf(objectGraph, cardMedia); + if (serverData.isDefinedNonNull(fallbackShelf)) { + shelves.push(fallbackShelf); + } + } + return shelves; + }); +} +// region Data Augmenting +/** + * Article specific entrypoint for page response augmenting. See `augment.ts`. + * @param response Response to augment. + */ +export async function fetchAdditionalDataForInitialResponse(objectGraph, response) { + return await mediaAugment.fetchAugmentedData(objectGraph, response, findAdditionalDataKeysForArticleResponse, fetchDataForArticleDataKey); +} +/** + * Determine the set of data, expressed as an set of `ArticleAdditionalDataKey`s, that need to be fetched for given article response to be displayed. + * This is equivalent to `AbstractMediaApiPageBuilder.additionalDataKeysNeededForData`, but for article builder which doesn't adopt the `builder` API. + * + * @param articleResponse Initial response to determine additional data requirements for. + * @returns {Set<ArticleAdditionalDataKey>} Additional data needed expressed as set of `ArticleAdditionalDataKey` + */ +function findAdditionalDataKeysForArticleResponse(objectGraph, articleResponse) { + /** + * Keys for requested requirements determined by: + * - Modules in canvas only now :) + */ + const allAdditionalDataKeySet = new Set(); + // Requirements based on canvas items: + const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse); + const canvasModules = mediaRelationships.relationshipCollection(articleData, "canvas"); + for (const storyModule of canvasModules) { + // Determine additional requests and add to `allRequirementsSet` + const dataKeysForModule = additionalDataKeysForArticleModule(objectGraph, storyModule, articleData); + if (serverData.isDefinedNonNullNonEmpty(dataKeysForModule)) { + for (const requirement of dataKeysForModule) { + allAdditionalDataKeySet.add(requirement); + } + } + } + return allAdditionalDataKeySet; +} +/** + * Builds a promise that will fetch data fulfilling given requirement. Note that these promises will return `null` when they fail, + * and their failure should not cause the entire page to fail. + * This is equivalent to `AbstractMediaApiPageBuilder.fetchAdditionalDataForKey`, but for article builder which doesn't adopt the `builder` API. + * + * @param dataKey Corresponding data key to fetch data for. + */ +// eslint-disable-next-line @typescript-eslint/promise-function-async +function fetchDataForArticleDataKey(objectGraph, dataKey) { + let request; + if (dataKey === "upsellForNonacquisitionCanvas") { + // Use `editorialItem` matching context of that would've otherwise been joined if this story was an acquisition story. + request = arcadeCommon.arcadeUpsellRequest(objectGraph, models.marketingItemContextFromString("editorialItemCanvas")); + } + if (dataKey === "arcadeIcons") { + // Require 10 for now. + request = arcadeCommon.arcadeAppsRequestForIcons(objectGraph, 10); + } + if (serverData.isNull(request)) { + return null; + } + // Failable data fetch, either resolving to valid response or `null`. + return mediaNetwork.fetchData(objectGraph, request).catch(() => null); +} +/** + * Determine the requirements for single article module as determined by it's type. + * @param storyModule The module to fetch additional requirements for. + * @param articleData The article that contains `storyModule` in its canvas. + * @returns {ArticleAdditionalDataKey[] | undefined} Set of data keys if any are needed for rendering given module. + */ +export function additionalDataKeysForArticleModule(objectGraph, storyModule, articleData) { + // Only `AppMarker` has additional requirements. + const moduleType = mediaAttributes.attributeAsString(storyModule, "displayType"); + if (moduleType !== "AppMarker") { + return null; + } + const markerType = mediaAttributes.attributeAsString(storyModule, "appMarkerType"); + // <rdar://problem/55919205> In story Arcade acquisition module dropping from stories + // Editorial wants to use the acquisition module in non-acquisition stories, but the `upsell` relationship is only joined for EIs marked with the acquisition flag. + // When an article is missing the upsell relationship, we'll fetch it separately if we have modules that need it... + const articleDataIsMissingUpsell = serverData.isNull(arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData)); + /** + * Acquisition AppMarker, i.e. `ArcadeShowcase` needs: + * 1. Upsell data for text data, e.g. editorial notes and breakoutCallToAction label, provided this data isn't already provided as part of original page. + * 2. Assortment of Arcade App Icons (iOS Only). + */ + const additionalDataKeysForModule = []; + if (markerType === "Acquisition") { + // iOS needs icon dependency + if (objectGraph.host.isiOS || objectGraph.client.isVision) { + additionalDataKeysForModule.push("arcadeIcons"); + } + // All platform needs upsell to render acquisition modules, add it if missing. + if (articleDataIsMissingUpsell) { + additionalDataKeysForModule.push("upsellForNonacquisitionCanvas"); + } + } + return additionalDataKeysForModule; +} +// endregion +/** + * Create a shelf model representing a single module within article pages. + * @param storyModule Module server data to build shelf and contents from. + * @param articleData The data for article that contains `storyModule` above. + * @param context Global parse context updated while entire sets of modules are being parsed. + * @returns an array of `Shelf` or `null` if building fails for given module. + */ +function renderModule(objectGraph, storyModule, articleData, context, shelfIndex) { + return validation.catchingContext(`module: ${context.module}`, () => { + var _a; + const shelves = []; + switch (context.module) { + case "Header": { + context.titleForNextShelf = mediaAttributes.attributeAsString(storyModule, "editorialCopy"); + break; + } + case "TextBlock": { + const textBlockShelf = createParagraph(objectGraph, storyModule, context); + if (isSome(textBlockShelf)) { + shelves.push(textBlockShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "CollectionLockup": { + const appListShelf = createAppList(objectGraph, storyModule, context); + if (isSome(appListShelf)) { + shelves.push(appListShelf); + context.hasFocusableElements = true; + } + break; + } + case "InlineImage": { + const inlineImageShelf = createImage(objectGraph, storyModule, context); + if (isSome(inlineImageShelf)) { + shelves.push(inlineImageShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "AppLockup": { + const appLockupShelf = createAppLockup(objectGraph, storyModule, context); + if (isSome(appLockupShelf)) { + shelves.push(appLockupShelf); + context.hasFocusableElements = true; + } + break; + } + case "TipBlock": { + const tipShelf = createTip(objectGraph, storyModule, context); + if (isSome(tipShelf)) { + shelves.push(tipShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "PullQuote": { + const pullQuoteShelf = createPullQuote(objectGraph, storyModule, context); + if (isSome(pullQuoteShelf)) { + shelves.push(pullQuoteShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "HorizontalRule": { + const horizontalRuleShelf = createHorizontalRule(objectGraph, storyModule, context); + if (isSome(horizontalRuleShelf)) { + shelves.push(horizontalRuleShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "InlineVideo": { + const inlineVideoShelf = createVideo(objectGraph, storyModule, context); + if (isSome(inlineVideoShelf)) { + shelves.push(inlineVideoShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppMedia": { + const appMediaShelf = createAppMedia(objectGraph, storyModule, context); + if (isSome(appMediaShelf)) { + shelves.push(appMediaShelf); + context.hasFocusableElements = true; + } + break; + } + case "LinkBlock": { + const linkBlockShelf = createLink(objectGraph, storyModule, context); + if (isSome(linkBlockShelf)) { + shelves.push(linkBlockShelf); + context.hasFocusableElements = true; + } + break; + } + case "TextList": { + const textListShelf = createTextList(objectGraph, storyModule, context); + if (isSome(textListShelf)) { + shelves.push(textListShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "IAPLockup": { + const iapLockupShelf = createIAPLockup(objectGraph, storyModule, context); + if (isSome(iapLockupShelf)) { + shelves.push(iapLockupShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppMarker": { + const appMarkerShelf = createAppMarker(objectGraph, storyModule, articleData, context); + if (isSome(appMarkerShelf)) { + shelves.push(appMarkerShelf); + context.hasFocusableElements = true; + } + break; + } + case "StoryList": { + const storyListShelf = createStoryCards(objectGraph, storyModule, context, shelfIndex); + if (isSome(storyListShelf)) { + shelves.push(storyListShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppEventLockup": { + const appEventShelf = createAppEventLockup(objectGraph, storyModule, context); + if (isSome(appEventShelf)) { + shelves.push(appEventShelf); + context.hasFocusableElements = true; + } + break; + } + case "OfferItemLockup": { + const offerItemShelves = createOfferItemLockup(objectGraph, storyModule, context); + if (isSome(offerItemShelves)) { + shelves.push(...offerItemShelves); + context.hasFocusableElements = true; + } + break; + } + default: { + objectGraph.console.log(`Unknown module: ${context.module}`); + } + } + for (const shelf of shelves) { + const existingShelfPresentationHints = (_a = shelf.presentationHints) !== null && _a !== void 0 ? _a : {}; + shelf.presentationHints = { + ...existingShelfPresentationHints, + isArticleContext: true, + }; + } + return shelves; + }); +} +const FULL_WIDTH_MODULES = ["AppLockup", "InlineImage", "InlineVideo", "AppMarker"]; +/** + * Determines whether the provided parameters signifies a full-width article + * module. + * @param shelf The shelf in question. + * @param type The type of article module. + * @returns Whether or not the given shelf for the article type is full width. + */ +function isArticleShelfFullWidth(objectGraph, shelf, type) { + if (shelf && type) { + const itemCount = shelf.items.length; + if (itemCount > 0 && FULL_WIDTH_MODULES.indexOf(type) !== -1) { + const lastItem = shelf.items[itemCount - 1]; + switch (shelf.contentType) { + case "framedArtwork": { + const framedArt = lastItem; + return framedArt && framedArt.isFullWidth; + } + case "framedVideo": { + const framedVideo = lastItem; + return framedVideo && framedVideo.isFullWidth; + } + default: { + return true; + } + } + } + } + return false; +} +// region Footer Lockup +/** + * Adds either a `footerLockup` or `arcadeFooterLockup` property on `ArticlePage` model, based on type of article. + * @param page Page to add footer to if needed. + * @param articleData Original data of article being rendered. + * @param context Parse context for article builder. + */ +function addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context) { + // App Lockup for Articles about single specific app. + const footerProductData = productDataFromArticle(objectGraph, articleData); + if (footerProductData) { + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, articleData); + page.footerLockup = productFooterLockupFromData(objectGraph, footerProductData, context, externalDeepLinkUrl); + return; + } + // Arcade Lockup for Acquisition Story for supported platforms + const isArcadeAcquisitionEI = mediaAttributes.attributeAsBooleanOrFalse(articleData, "isAcquisition"); + const platformSupportsArcadeFooterLockup = objectGraph.host.isiOS || objectGraph.host.isMac; + const additionalDataIsAvailable = serverData.isDefinedNonNull(context.additionalData); + if (additionalDataIsAvailable && isArcadeAcquisitionEI && platformSupportsArcadeFooterLockup) { + const upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData); + page.arcadeFooterLockup = arcadeFooterLockupFromData(objectGraph, upsellData, context); + } +} +/** + * Find platform data from editorial item to enhance sharing and display in footer lockup + * At the moment, only single app editorials get footer lockups and have enhanced sharing. + * + * @param editorialItem Item to find footer content for + * @returns content to display in footer lockup, or null if no content should be displayed + */ +export function productDataFromArticle(objectGraph, editorialItem) { + const relatedContent = mediaRelationships.relationshipCollection(editorialItem, "card-contents"); + if (relatedContent.length !== 1) { + return null; + } + const contentData = relatedContent[0]; + if (!contentData) { + return null; + } + switch (contentData.type) { + case "apps": + case "app-bundles": + return contentData; + default: + return null; + } +} +/** + * Creates a footer lockup with a data for a specific app. + * Cover method over `lockupFromData` to override `offerStyle`. + * + * @param data MAPI data to build footer with. + * @param context Parse context + * @param externalDeepLinkUrl promotional deep link url to use on the lockup's offer. + * @returns A new `Lockup` object for footer lockups. + */ +function productFooterLockupFromData(objectGraph, data, context, externalDeepLinkUrl) { + const lockupOptions = { + offerStyle: footerLockupOfferStyle(objectGraph), + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: context.crossLinkSubtitle, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "smallLockup"), + }; + return lockups.lockupFromData(objectGraph, data, lockupOptions); +} +/** + * Creates a footer lockup representing the Arcade subscription service. + * @param upsellData Contains both editorial and iAP data for Arcade + * @param context Parse context. + */ +function arcadeFooterLockupFromData(objectGraph, upsellData, context) { + const metricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + return lockups.arcadeLockupFromData(objectGraph, upsellData, metricsOptions, models.marketingItemContextFromString("editorialItem"), "infer", null); +} +/** + * Determines the offer style to use for the footer lockup. + */ +function footerLockupOfferStyle(objectGraph) { + switch (objectGraph.client.deviceType) { + case "mac": + return "white"; + default: + return "infer"; + } +} +// endregion +function createFallbackListShelf(objectGraph, cardMedia) { + if (cardMedia instanceof models.TodayCardMediaList || cardMedia instanceof models.TodayCardMediaRiver) { + const fallbackShelf = new models.Shelf("smallLockup"); + fallbackShelf.items = cardMedia.lockups; + if (objectGraph.client.isWeb) { + fallbackShelf.presentationHints = { + ...fallbackShelf.presentationHints, + isArticleContext: true, + }; + } + return fallbackShelf; + } + return null; +} +function shareSheetActionFromData(objectGraph, editorialItem, todayCardConfig) { + const productData = productDataFromArticle(objectGraph, editorialItem); + /* + * Determine title + */ + let title = null; + const name = content.notesFromData(objectGraph, editorialItem, "name"); + const short = content.notesFromData(objectGraph, editorialItem, "short"); + // Prefer "name: short" + if (name && short) { + title = objectGraph.loc + .string("ShareSheet.TitleSubtitle.Format", "{title}: {subtitle}") + .replace("{title}", name) + .replace("{subtitle}", short); + } + // Followed by name + if (!title && name) { + title = name; + } + // Followed by short + if (!title && short) { + title = short; + } + // Followed by product name + if (!title && productData) { + const productTitle = mediaAttributes.attributeAsString(productData, "name"); + const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.GameOfTheDay: { + title = objectGraph.loc.string("SHARE_SHEET_GAME_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle); + break; + } + case TodayCardDisplayStyle.AppOfTheDay: { + title = objectGraph.loc.string("SHARE_SHEET_APP_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle); + break; + } + default: { + objectGraph.console.log(`No title for article with unknown style: ${cardDisplayStyle}`); + break; + } + } + } + const url = mediaAttributes.attributeAsString(editorialItem, "url"); + let articleArtwork; + const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.Grid: + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.River: + articleArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://ShareCollectionThumbnail", 40, 40); + break; + default: + articleArtwork = null; + break; + } + // Create share sheet model (bail out if unable to do so) + const shareData = sharing.shareSheetDataForArticle(objectGraph, title, url, null, articleArtwork, editorialItem); + if (!serverData.isDefinedNonNull(shareData)) { + return null; + } + const activities = sharing.shareSheetActivitiesForArticle(objectGraph, url, todayCardPreviewUrlForTodayCard(objectGraph, editorialItem.id, todayCardConfig), editorialItem.id); + return new models.ShareSheetAction(shareData, activities); +} +function createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth) { + if (!serverData.isDefinedNonNull(shareAction) || + objectGraph.client.isVision || + preprocessor.GAMES_TARGET || + objectGraph.client.isCompanionVisionApp) { + return null; + } + // Create share button + const shareButton = new models.RoundedButton("share", objectGraph.loc.string("SHARE_STORY"), !isLastModuleFullWidth, shareAction); + // Add share shelf + const shareButtonShelf = new models.Shelf("roundedButton"); + shareButtonShelf.items = [shareButton]; + return shareButtonShelf; +} +function createParagraph(objectGraph, module, context) { + const text = mediaAttributes.attributeAsString(module, "editorialCopy"); + if (!text) { + return null; + } + const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, paragraph, context); + const shelf = new models.Shelf("paragraph"); + shelf.items = [paragraph]; + return shelf; +} +function createImage(objectGraph, module, context) { + const displayStyle = mediaAttributes.attributeAsString(module, "inlineImageDisplayType"); + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + // If the displayStyle is FullWidth want to 'allowTransparency' so that images blend into the page in both + // light and dark mode. Previously editorial would bake white backgrounds into images they wanted to 'blend' + // with the page + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + allowingTransparency: displayStyle === "FullWidth" && !objectGraph.client.isVision, + withJoeColorPlaceholder: objectGraph.client.isVision, + }); + if (!artwork) { + return null; + } + const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml"); + // Get the optional caption + frame.caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + context.subStyle = displayStyle; + if (displayStyle) { + switch (displayStyle) { + case "BoundingBox": { + frame.isFullWidth = false; + frame.hasRoundedCorners = true; + break; + } + case "FullWidth": + default: { + frame.isFullWidth = true; + frame.hasRoundedCorners = false; + break; + } + } + } + // Setup impressions + addImpressionsFieldsToModel(objectGraph, frame, context); + const shelf = new models.Shelf("framedArtwork"); + shelf.items = [frame]; + return shelf; +} +function createTip(objectGraph, module, context) { + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + if (!artwork) { + return null; + } + const caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + const ordinal = mediaAttributes.attributeAsString(module, "tipNumber"); + // Create the tip image + const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml"); + frame.isFullWidth = false; + frame.hasRoundedCorners = true; + frame.caption = caption; + frame.ordinal = ordinal; + // Setup impressions + addImpressionsFieldsToModel(objectGraph, frame, context); + // Create the shelf + const shelf = new models.Shelf("framedArtwork"); + shelf.items = [frame]; + return shelf; +} +function createPullQuote(objectGraph, module, context) { + const text = mediaAttributes.attributeAsString(module, "quote"); + const attribution = mediaAttributes.attributeAsString(module, "quoteAttribution"); + // Get the optional artwork + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + const fullWidth = mediaAttributes.attributeAsString(module, "pullQuoteDisplayType") === "FullWidth"; + // Create the quote + const quote = new models.Quote(text, attribution, artwork, fullWidth); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, quote, context); + // Create the shelf + const shelf = new models.Shelf("quote"); + shelf.items = [quote]; + return shelf; +} +function createHorizontalRule(objectGraph, module, context) { + const lineStyle = mediaAttributes.attributeAsString(module, "lineStyle"); + const fullWidth = mediaAttributes.attributeAsString(module, "displayStyle") === "FullWidth"; + let ruleColor = color.named("defaultLine"); + if (objectGraph.client.isVision && (lineStyle === "Dotted" || lineStyle === "Dashed")) { + ruleColor = color.white; + } + // Parse the customColor from Media API. This can only be a dynamic color. + const apiColor = mediaAttributes.attributeAsDictionary(module, "customColor"); + const lightColor = color.fromHex(serverData.asString(apiColor, "lightMode")); + const darkColor = color.fromHex(serverData.asString(apiColor, "darkMode")); + if (!serverData.isNullOrEmpty(lightColor) && !serverData.isNullOrEmpty(darkColor)) { + ruleColor = color.dynamicWith(lightColor, darkColor); + } + const horizontalRule = new models.HorizontalRule(lineStyle, ruleColor, fullWidth); + // Create the Shelf + const shelf = new models.Shelf("horizontalRule"); + shelf.items = [horizontalRule]; + return shelf; +} +function createVideo(objectGraph, module, context) { + // Get the preview artwork + const artworkData = mediaAttributes.attributeAsDictionary(module, "video.previewFrame"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + if (!artwork) { + return null; + } + // Get the video URL + const videoUrl = mediaAttributes.attributeAsString(module, "video.video"); + if (!videoUrl) { + return null; + } + const videoDisplayType = mediaAttributes.attributeAsString(module, "inlineVideoDisplayType"); + const isFullWidth = videoDisplayType === "FullWidth"; + // Create the video + const video = new models.Video(videoUrl, artwork, videoDefaults.defaultVideoConfiguration(objectGraph)); + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + const videoModule = new models.FramedVideo(video, isFullWidth, "text/x-apple-as3-nqml"); + // Get the optional caption + videoModule.caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, videoModule, context); + // Create the shelf + const shelf = new models.Shelf("framedVideo"); + shelf.items = [videoModule]; + return shelf; +} +function createAppLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Shelf to generate. Either lockup, app showcase, or app event shelf + let shelf = null; + // If we have an app-events relationship, we want to use this as the priority. This sometimes exists on the + // AppLockup type, rather than as the AppEventLockup type, so that older clients can still render this + // item by falling back to the AppLockup type. + const appEventsDataItems = mediaRelationships.relationshipCollection(module, "app-events"); + if (serverData.isDefinedNonNullNonEmpty(appEventsDataItems)) { + shelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, appEventsDataItems, context.metricsPageInformation, context.metricsLocationTracker, context); + if (serverData.isDefinedNonNull(shelf)) { + return shelf; + } + } + // Set the display style + const displayStyle = mediaAttributes.attributeAsString(module, "appLockupSize"); + context.subStyle = displayStyle; + let shelfStyle; + let isLockup = false; + if (displayStyle) { + switch (displayStyle) { + case "Small": { + shelfStyle = "smallLockup"; + isLockup = true; + break; + } + case "Medium": { + shelfStyle = "mediumLockup"; + isLockup = true; + break; + } + case "Large": + default: { + if (objectGraph.client.isWatch || + objectGraph.client.isTV || + objectGraph.client.isVision || + preprocessor.GAMES_TARGET) { + // Per design, on watchOS we always show a lockup for app showcases. + // Watch App Store treats all lockup sizes the same -- let's pick small. + shelfStyle = "smallLockup"; + isLockup = true; + } + else { + shelfStyle = "appShowcase"; + } + break; + } + } + } + // Determine the deep link URL, if there is one. + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module); + // Create the appropriate shelf item + if (isLockup) { + const lockupShelf = new models.Shelf(shelfStyle); + const metricsOptions = { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfStyle), + }; + let lockup; + if (preprocessor.GAMES_TARGET) { + const shelfID = new PageID(context.pageId).shelfID(module.id); + lockup = gamesComponentBuilder.makeArticleGameLockup(objectGraph, contentData, shelfID); + } + else { + lockup = lockups.lockupFromData(objectGraph, contentData, metricsOptions); + } + if (isNothing(lockup)) { + return null; + } + lockupShelf.items = [lockup]; + shelf = lockupShelf; + } + else { + // On all platforms, the AppLockup platform generates a AppShowcase when display style is large. + shelf = createAppShowcase(objectGraph, module, context); + } + return shelf; +} +function createAppShowcase(objectGraph, module, context) { + // Create the shelf + const shelf = new models.Shelf("appShowcase"); + // Parameterize by platform: + // tvOS populates the `screenshots` field to display alongside video. + const showcaseHasScreenshots = objectGraph.client.isTV; + // Only non-tvOS has shelf background color + const shelfHasBackgroundColor = objectGraph.client.deviceType !== "tv"; + const contentData = contentFromModule(objectGraph, module, context); + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module); + const lockup = lockups.lockupFromData(objectGraph, contentData, { + offerStyle: "colored", + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: context.crossLinkSubtitle, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + const showcase = new models.AppShowcase("large", lockup); + showcase.description = lockups.subtitleFromData(objectGraph, contentData); + // Add Video + // Configure the video for the showcase, if the module demands it. + let showcaseVideo = null; + const videoType = mediaAttributes.attributeAsString(module, "appLockupVideo"); + switch (videoType) { + case "AppTrailer": { + const allAppVideos = content.videoPreviewsFromData(objectGraph, contentData); + if (allAppVideos && allAppVideos.length > 0) { + showcaseVideo = allAppVideos[0]; + } + break; + } + default: + break; + } + if (showcaseVideo) { + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, showcaseVideo, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + showcase.video = showcaseVideo; + } + // Add Screenshots for AppShowcase if necessary + if (showcaseHasScreenshots) { + showcase.screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [content.currentAppPlatform(objectGraph)]); + } + // Configure background if necessary. + if (shelfHasBackgroundColor) { + shelf.background = { + type: "color", + color: appShowcaseBackgroundColor, + }; + } + shelf.items = [showcase]; + return shelf; +} +function createIAPLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Create the lockup + const lockup = lockups.inAppPurchaseLockupFromData(objectGraph, contentData, { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + if (!lockup) { + return null; + } + const showcase = new models.InAppPurchaseShowcase(lockup); + // Create the shelf + const shelf = new models.Shelf("inAppPurchaseShowcase"); + shelf.background = { + type: "color", + color: iAPBackgroundColor, + }; + shelf.items = [showcase]; + return shelf; +} +function createAppList(objectGraph, module, context) { + const showOrdinals = mediaAttributes.attributeAsBooleanOrFalse(module, "showOrdinals"); + const ordinalDirection = mediaAttributes.attributeAsString(module, "collectionLockupDisplayType") === "OrdinalDesc" + ? "descending" + : "ascending"; + // Set the display style + const displayStyle = mediaAttributes.attributeAsString(module, "collectionLockupSize"); + context.subStyle = displayStyle; + let style; + if (displayStyle) { + switch (displayStyle) { + case "Large": { + style = "largeLockup"; + break; + } + case "Medium": { + style = "mediumLockup"; + break; + } + case "Small": + default: { + style = "smallLockup"; + break; + } + } + } + // Construct the lockup options + const lockupOptions = { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, style), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, style), + }; + // Check if we have content + const contents = mediaRelationships.relationshipCollection(module, "contents"); + if (isNothing(contents)) { + return null; + } + let childLockups = []; + if (preprocessor.GAMES_TARGET) { + const shelfID = new PageID(context.pageId).shelfID(module.id); + childLockups = gamesComponentBuilder.makeArticleGameLockups(objectGraph, contents, shelfID); + } + else { + childLockups = lockups.lockupsFromData(objectGraph, contents, { + includeOrdinals: showOrdinals, + ordinalDirection: ordinalDirection, + lockupOptions: lockupOptions, + }); + } + if (!childLockups || childLockups.length === 0) { + return null; + } + // Create the shelf + const shelf = new models.Shelf(style); + shelf.items = childLockups; + return shelf; +} +function createAppMedia(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Set the display style + const mediaOption = mediaAttributes.attributeAsString(module, "appMediaOption"); + const appMediaPlatform = mediaAttributes.attributeAsString(module, "appMediaPlatform"); + context.subStyle = mediaOption; + switch (mediaOption) { + case "Screenshots": { + let shelf = null; + // I'm so sorry, but making this split makes the macOS client code infinitely better because we are able + // to reuse the same product media view and component contract that is used on product page screenshots/trailers. + // Really, iOS should be reworked such that its module & product page implementation has a single source, + // but this has serious design obstacles that need to be worked through. + if (objectGraph.client.isMac) { + shelf = new models.Shelf("productMedia"); + const productMedia = content.productMediaFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */); + if (serverData.isDefinedNonNull(productMedia) && productMedia.length) { + shelf.items = productMedia; + } + } + else { + shelf = new models.Shelf("screenshots"); + if (serverData.isNull(appMediaPlatform)) { + /** + * The server did not tell us which app platform to use, so we need to infer based on various keys in + * the response. These parameters are only fully baked into product-dv responses, so we we need to do + * the more expensive product-dv lookup in order to correctly infer the default screenshots to use for + * the shelf. + */ + const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */); + if (screenshots && screenshots.length > 0) { + shelf.items = [screenshots[0]]; + } + } + else { + /** + * Server tells us which platform to use -- dictated by `appMediaPlatform`. Selectively do a lookup for + * just those screenshots. + */ + const desiredAppPlatform = appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform); + if (desiredAppPlatform) { + const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [desiredAppPlatform]); + if (screenshots && screenshots.length) { + shelf.items = [screenshots[0]]; + } + } + } + } + if (serverData.isDefinedNonNull(shelf) && shelf.items.length === 0) { + return null; + } + return shelf; + } + case "AppTrailers": + const trailersShelf = new models.Shelf("framedVideo"); + const videoPreviews = content.videoPreviewsFromData(objectGraph, contentData); + if (videoPreviews && videoPreviews.length > 0) { + const video = videoPreviews[0]; + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + const firstTrailer = new models.FramedVideo(video, false, "text/plain", null, null, true); + trailersShelf.items = [firstTrailer]; + return trailersShelf; + } + else { + return null; + } + default: { + return null; + } + } +} +function createLink(objectGraph, module, context) { + if (objectGraph.client.isTV || objectGraph.client.isWatch) { + return null; + } + const urlString = mediaAttributes.attributeAsString(module, "url"); + if (!urlString) { + return null; + } + const url = new urls.URL(urlString); + const linkTitle = mediaAttributes.attributeAsString(module, "urlTitle"); + let text = mediaAttributes.attributeAsString(module, "editorialCopy"); + if (!text) { + text = url.host; + } + const mediaHosts = [ + "itunes.apple.com", + "apps.apple.com", + "music.apple.com", + "books.apple.com", + "podcasts.apple.com", + "watch-app.cdn-apple.com", + "tv.apple.com", + ]; + let linkPresentationEnabled = false; + for (const mediaHost of mediaHosts) { + if (url.host.endsWith(mediaHost)) { + linkPresentationEnabled = true; + } + } + const action = new models.ExternalUrlAction(urlString); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + targetType: "link", + pageInformation: context.metricsPageInformation, + id: `${context.index}`, + locationTracker: context.metricsLocationTracker, + }); + const link = new models.EditorialLink(linkTitle, text, action, linkPresentationEnabled); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, link, context); + const shelf = new models.Shelf("editorialLink"); + shelf.items = [link]; + return shelf; +} +function createTextList(objectGraph, module, context) { + const listEntries = mediaAttributes.attributeAsArrayOrEmpty(module, "editorialCopy"); + if (!listEntries.length) { + return null; + } + const type = mediaAttributes.attributeAsString(module, "textListDisplayType"); + context.subStyle = type; + let isBulleted = false; + switch (type) { + case "Bulleted": { + isBulleted = true; + break; + } + default: { + isBulleted = false; + break; + } + } + let text; + if (isBulleted) { + text = "<ul>"; + } + else { + text = "<ol>"; + } + for (const textEntry of listEntries) { + const listItemJSONString = JSON.stringify(textEntry); + // rdar://104446319 - We must use `parse` on our JSON string to convert back to + // a raw string object as this ensures leading/trailing quotation marks are *not* escaped + const listItem = JSON.parse(listItemJSONString); + text = `${text}<li>${listItem}</li>`; + } + if (isBulleted) { + text = `${text}</ul>`; + } + else { + text = `${text}</ol>`; + } + const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, paragraph, context); + const shelf = new models.Shelf("paragraph"); + shelf.items = [paragraph]; + return shelf; +} +function createStoryCards(objectGraph, module, context, shelfIndex) { + if (objectGraph.client.isVision) { + const shelfToken = createBaseShelfToken(objectGraph, undefined, module, false, shelfIndex, context.metricsPageInformation, context.metricsLocationTracker); + const shelf = buildSmallStoryCardShelf(objectGraph, shelfToken); + shelf.isHorizontal = true; + return shelf; + } + const cards = mediaRelationships.relationshipCollection(module, "contents"); + if (!cards) { + return null; + } + const title = mediaAttributes.attributeAsString(module, "name"); + const subtitle = mediaAttributes.attributeAsString(module, "tagline"); + let shelf = null; + if (objectGraph.client.isiOS && objectGraph.featureFlags.isEnabled("mini_today_cards_article")) { + const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker); + shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, cards, title, subtitle, todayParseContext); + } + else { + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + const resolvedContentType = isSmallStoryCardsSupported ? "smallStoryCard" : "todayBrick"; + shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, cards, resolvedContentType, title, subtitle, context, null); + if (isSmallStoryCardsSupported) { + // Only specific small story cards are supported and will crash otherwise, filter those here preemptively. + // rdar://91965501 (MAS Crashing - Earth Day Landing Page - 4/19) + if (Array.isArray(shelf.items)) { + shelf.items = shelf.items.filter((item) => { + if (!(item instanceof models.TodayCard)) { + return true; + } + return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, resolvedContentType); + }); + } + } + } + return shelf; +} +function createAppEventLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + return appPromotionsShelf.appEventsShelfForArticle(objectGraph, [contentData], context.metricsPageInformation, context.metricsLocationTracker, context); +} +function createOfferItemLockup(objectGraph, module, context) { + if (!objectGraph.client.isiOS) { + return []; + } + const offerItem = mediaRelationships.relationshipData(objectGraph, module, "contents"); + if (serverData.isNullOrEmpty(offerItem)) { + return null; + } + // Offer detail Paragraph + const offerParagraph = mediaAttributes.attributeAsString(module, "editorialCopy"); + const paragraph = new models.Paragraph(offerParagraph, "text/x-apple-as3-nqml", "article"); + const paragraphShelf = new models.Shelf("paragraph"); + paragraphShelf.items = [paragraph]; + // Winback Offer Card + const offerItemShelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, [offerItem], context.metricsPageInformation, context.metricsLocationTracker, context); + return [paragraphShelf, offerItemShelf]; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * attributes: { + * displayType: "AppMarker", + * appMarkerType: <AppMarkerType> + * } + * } + * to generate an shelf for AppMarker model. + */ +function createAppMarker(objectGraph, appMarkerModule, articleData, context) { + const markerType = mediaAttributes.attributeAsString(appMarkerModule, "appMarkerType"); + context.subStyle = markerType; + let shelf = null; + switch (markerType) { + case "OSUpgrade": + shelf = createOSUpgradeClientControlButton(objectGraph, appMarkerModule, context); + break; + case "Acquisition": + shelf = createArcadeShowcase(objectGraph, appMarkerModule, articleData, context); + break; + default: + break; + } + return shelf; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * attributes: { + * displayType: "AppMarker", + * appMarkerType: "OSUpgrade" + * } + * } + * to generate an shelf with an button that links to preferences updates. + */ +function createOSUpgradeClientControlButton(objectGraph, osUpgradeModule, context) { + const deviceType = objectGraph.client.deviceType; + if (deviceType !== "mac") { + return null; // Early exit - Only MAS utilizes OS Upgrade Client Control Button currently. + } + const installUpdateUrl = links.osUpdateUrl(deviceType); + if (installUpdateUrl === null) { + return null; + } + // Action to Preferences + const openUpdatesAction = new models.ExternalUrlAction(installUpdateUrl); + // Action to open preferences is configured as `link` + metricsHelpersClicks.addClickEventToAction(objectGraph, openUpdatesAction, { + targetType: "link", + pageInformation: context.metricsPageInformation, + id: `${context.index}`, + locationTracker: context.metricsLocationTracker, + }); + // Shelf model + const upgradeControlText = objectGraph.loc.string("CLIENT_CONTROL_OS_UPGRADE_TITLE", "CHECK FOR UPDATE"); + const upgradeControl = new models.ClientControlButton(upgradeControlText, openUpdatesAction); + // Add impressions + addImpressionsFieldsToModel(objectGraph, upgradeControl, context); + const shelf = new models.Shelf("clientControlButton"); + shelf.items = [upgradeControl]; + return shelf; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * } + * with additional data: + * - Upsell data on `context.additionalData` + * - Icon Artwork data (iOS only) on `context.additionalData` + * + * to generate an shelf that promotes Arcade service. + * + * @param arcadeShowcaseModule Arcade showcase module + * @param articleData The data backing the article containing the module. Used for top-level relationship. + * @param context Parse context for this page parsing. This context contains the additional requirements data. + */ +function createArcadeShowcase(objectGraph, arcadeShowcaseModule, articleData, context) { + const supportedOnPlatform = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision; + if (!supportedOnPlatform) { + return null; + } + // Default to upsell on relation, falling back to upsell that may have been fetched separately for orphaned acquisition modules. + let upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData); + if (!upsellData && context.additionalData) { + const upsellResponse = context.additionalData.get("upsellForNonacquisitionCanvas"); + upsellData = arcadeCommon.upsellFromContentsOfUpsellResponse(objectGraph, upsellResponse); + } + if (!serverData.isDefinedNonNull(upsellData)) { + return null; + } + const baseMetricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + // Flow to See All games if subscribed + const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision); + if (preprocessor.GAMES_TARGET) { + subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + // Flow to Arcade Subscribe page if unsubscribed. + let unsubscribedAction; + const unsubscribedActionTitle = breakoutsCommon.callToActionLabelFromData(objectGraph, upsellData.marketingItemData); + if (serverData.isDefinedNonNullNonEmpty(unsubscribedActionTitle)) { + // We support an inline offer here instead, when the pricing token is there. + unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, models.marketingItemContextFromString("editorialItemCanvas"), baseMetricsOptions); + if (serverData.isDefinedNonNull(unsubscribedAction)) { + unsubscribedAction.title = unsubscribedActionTitle; + } + } + else { + // If Upsell EI is misconfigured and missing `breakoutCallToActionLabel`, default to opening Arcade app for unsubscribed state. + unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision); + if (preprocessor.GAMES_TARGET) { + unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + } + const arcadeShowcase = new models.ArcadeShowcase(unsubscribedAction, subscribedAction); + const unsubscribedDescription = arcadeUpsell.descriptionFromData(objectGraph, upsellData.marketingItemData); + arcadeShowcase.unsubscribedDescription = unsubscribedDescription; + const offerDisplayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, "colored", null, "dark", null, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId); + if (preprocessor.GAMES_TARGET) { + offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + arcadeShowcase.offerDisplayProperties = offerDisplayProperties; + const showcaseMetricsOptions = { + ...baseMetricsOptions, + targetType: "arcadeShowcase", + title: unsubscribedActionTitle, + id: arcadeShowcaseModule.id, + kind: "arcadeShowcase", + softwareType: null, + displaysArcadeUpsell: true, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, arcadeShowcase, showcaseMetricsOptions); + // Build Artwork for iOS only + if (objectGraph.host.isiOS || objectGraph.client.isVision) { + // Context should have additional data to source icons. + if (serverData.isNull(context.additionalData)) { + return null; + } + const iconResponse = context.additionalData.get("arcadeIcons"); + if (serverData.isDefinedNonNullNonEmpty(iconResponse)) { + const iconMetricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + const iconsDataCollection = mediaDataStructure.dataCollectionFromResultsListContainer(iconResponse); + arcadeShowcase.iconArtworks = content.impressionableAppIconsFromDataCollection(objectGraph, iconsDataCollection, iconMetricsOptions, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + } + } + const shelf = new models.Shelf("arcadeShowcase"); + shelf.items = [arcadeShowcase]; + const shelfHasBackgroundColor = objectGraph.host.isiOS || objectGraph.client.isVision; + if (shelfHasBackgroundColor) { + shelf.background = { + type: "color", + color: arcadeShowcaseShelfBackgroundColor, + }; + } + return shelf; +} +// endregion +function contentFromModule(objectGraph, module, context) { + const contents = mediaRelationships.relationshipData(objectGraph, module, "contents"); + if (!contents) { + return null; + } + return contents; +} +function addImpressionsFieldsToModel(objectGraph, model, context, impressionData) { + if (!model) { + return; + } + let impressionType = context.module; + if (context.subStyle) { + impressionType = impressionType + "_" + context.subStyle; + } + if (serverData.isNull(impressionData)) { + impressionData = { + id: `${context.index}`, + impressionIndex: context.index, + idType: "sequential", + impressionType: impressionType, + kind: "iosModule", + }; + } + model.impressionMetrics = new models.ImpressionMetrics(metricsHelpersUtil.sanitizedMetricsDictionary(impressionData)); +} +//# sourceMappingURL=article.js.map
\ No newline at end of file |
