summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/today/article.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /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.js1572
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