this repo has no description
1/**
2 * Created by keithpk on 3/21/17.
3 */
4import { isNothing, isSome } from "@jet/environment";
5import * as validation from "@jet/environment/json/validation";
6import * as models from "../../api/models";
7import * as serverData from "../../foundation/json-parsing/server-data";
8import * as mediaAttributes from "../../foundation/media/attributes";
9import * as mediaAugment from "../../foundation/media/augment";
10import * as mediaDataStructure from "../../foundation/media/data-structure";
11import * as mediaNetwork from "../../foundation/media/network";
12import * as mediaRelationships from "../../foundation/media/relationships";
13import * as urls from "../../foundation/network/urls";
14import * as color from "../../foundation/util/color-util";
15import { PageID } from "../../gameservicesui/src/common/id-builder";
16import * as gamesComponentBuilder from "../../gameservicesui/src/editorial-page/editorial-component-builder";
17import * as appPromotionsShelf from "../app-promotions/app-promotions-shelf";
18import * as arcadeCommon from "../arcade/arcade-common";
19import * as arcadeUpsell from "../arcade/arcade-upsell";
20import * as breakoutsCommon from "../arcade/breakouts-common";
21import * as videoDefaults from "../constants/video-constants";
22import * as artworkBuilder from "../content/artwork/artwork";
23import * as content from "../content/content";
24import { EditorialMediaPlacement } from "../editorial-pages/editorial-media-util";
25import { buildSmallStoryCardShelf } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder";
26import { buildStoryCard } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils";
27import { createBaseShelfToken } from "../editorial-pages/editorial-page-shelf-token";
28import { CollectionShelfDisplayStyle } from "../editorial-pages/editorial-page-types";
29import * as externalDeepLink from "../linking/external-deep-link";
30import * as links from "../linking/os-update-links";
31import * as lockups from "../lockups/lockups";
32import * as metricsHelpersClicks from "../metrics/helpers/clicks";
33import * as metricsHelpersImpressions from "../metrics/helpers/impressions";
34import * as metricsHelpersLocation from "../metrics/helpers/location";
35import * as metricsHelpersMedia from "../metrics/helpers/media";
36import * as metricsHelpersPage from "../metrics/helpers/page";
37import * as metricsHelpersUtil from "../metrics/helpers/util";
38import * as sharing from "../sharing";
39import { crossLinkSubtitleFromData, defaultTodayCardConfiguration, fallbackWatchTodayCardFromData, todayCardFromData, } from "./today-card-util";
40import * as todayHorizontalCardUtil from "./today-horizontal-card-util";
41import { todayCardPreviewUrlForTodayCard } from "./today-parse-util";
42import { HeroMediaDisplayContext, TodayCardDisplayStyle, TodayParseContext, } from "./today-types";
43export const iAPBackgroundColor = color.named("componentBackgroundStandout");
44const appShowcaseBackgroundColor = color.named("componentBackgroundStandout");
45const arcadeShowcaseShelfBackgroundColor = color.named("componentBackgroundStandout");
46/**
47 * Resolves the article module's app media platform to an `AppPlatform` to use for screenshots.
48 * @param {AppMediaPlatform} appMediaPlatform The server-dictated media platform to use for the module.
49 * @returns {AppPlatform} The app platform that is appropriate for this media platform, taking into account our device.
50 */
51function appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform) {
52 switch (appMediaPlatform) {
53 case "Watch":
54 return "watch";
55 case "iOS":
56 if (objectGraph.client.isPad) {
57 return "pad";
58 }
59 else {
60 return "phone";
61 }
62 case "tvOS":
63 return "tv";
64 case "Messages":
65 return "messages";
66 case "visionOS":
67 return "vision";
68 default:
69 return null;
70 }
71}
72export class ArticleParseContext {
73 constructor() {
74 // The index of the current module
75 this.index = 0;
76 // The reco metrics from the shelf on the today page
77 this.todayShelfRecoMetricsData = {};
78 /// Whether there are any focusable elements (for touch mode)
79 this.hasFocusableElements = false;
80 /// Whether there are any non-focusable elements (for touch mode)
81 this.hasNonFocusableElements = false;
82 /// Whether there is a resilient deep link.
83 this.isResilientDeepLink = false;
84 /// Whether or not to allow app event previews, used by editorial to preview app event stories before they are published
85 this.allowUnpublishedAppEventPreviews = false;
86 }
87}
88function todayCardConfigFromArticleContext(objectGraph, articleContext) {
89 if (!serverData.isDefinedNonNull(articleContext)) {
90 return null;
91 }
92 if (isSome(articleContext.todayCardConfig)) {
93 return articleContext.todayCardConfig;
94 }
95 const config = defaultTodayCardConfiguration(objectGraph);
96 config.enableListCardToMultiAppFallback = false;
97 config.clientIdentifierOverride = articleContext.clientIdentifierOverride;
98 config.useOTDTextStyle = false;
99 config.allowUnpublishedAppEventPreviews = articleContext.allowUnpublishedAppEventPreviews;
100 config.currentRowIndex = undefined;
101 switch (objectGraph.client.deviceType) {
102 case "mac":
103 config.prevailingCropCodes = { defaultCrop: "en" };
104 config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid;
105 config.heroDisplayContext = HeroMediaDisplayContext.Article;
106 break;
107 case "tv":
108 config.prevailingCropCodes = {
109 "defaultCrop": "ek",
110 "editorialArtwork.storyCenteredStatic16x9": "SCS.ApDHXL01",
111 };
112 config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid;
113 config.heroDisplayContext = HeroMediaDisplayContext.Article;
114 break;
115 case "web":
116 config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.River;
117 config.prevailingCropCodes = {
118 "defaultCrop": "sr",
119 "editorialArtwork.dayCard": "grav.west",
120 };
121 break;
122 default:
123 break;
124 }
125 return config;
126}
127export function articlePageFromResponse(objectGraph, articleResponse, context) {
128 return validation.context("articlePageWithResponse", () => {
129 var _a;
130 const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse);
131 context.metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "editorialItem", articleData.id, articleResponse);
132 context.metricsLocationTracker = metricsHelpersLocation.newLocationTracker();
133 context.pageId = articleData.id;
134 // Bridge over article contexts to today's metrics context and card config
135 const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker, context.refreshController);
136 const todayCardConfig = todayCardConfigFromArticleContext(objectGraph, context);
137 // Render the top card
138 let todayCard = todayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext);
139 let editorialStoryCard = null;
140 const todayCardMedia = todayCard === null || todayCard === void 0 ? void 0 : todayCard.media;
141 if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) {
142 editorialStoryCard = buildStoryCard(objectGraph, articleData, EditorialMediaPlacement.StoryDetail, {
143 pageInformation: context.metricsPageInformation,
144 locationTracker: context.metricsLocationTracker,
145 }, CollectionShelfDisplayStyle.StoryMedium, false);
146 todayCard = null;
147 }
148 if (isNothing(todayCard)) {
149 todayCard = fallbackWatchTodayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext);
150 }
151 // Get the title for metrics purposes.
152 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;
153 const editorialItemKind = mediaAttributes.attributeAsString(articleData, "kind");
154 // Configure subtitle for cross link
155 context.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, articleData);
156 // Bridge today config back into articles, now that cards are created.
157 // Now we've created the card, reference the clientIdentifierOverride it used for the rest of the article.
158 context.clientIdentifierOverride = todayCardConfig.clientIdentifierOverride;
159 // Start a metrics location
160 metricsHelpersLocation.pushContentLocation(objectGraph, {
161 pageInformation: context.metricsPageInformation,
162 locationTracker: context.metricsLocationTracker,
163 targetType: "article",
164 id: context.pageId,
165 idType: "its_id",
166 }, title);
167 // Render the article itself
168 const shelves = renderArticle(objectGraph, articleData, todayCardMedia, context);
169 const lastShelf = shelves[shelves.length - 1];
170 // Sharing
171 const shareAction = objectGraph.client.isTV ||
172 objectGraph.client.isWeb ||
173 context.isResilientDeepLink ||
174 preprocessor.GAMES_TARGET ||
175 editorialItemKind === "OfferItem"
176 ? null
177 : shareSheetActionFromData(objectGraph, articleData, todayCardConfig);
178 if (serverData.isDefinedNonNull(shareAction)) {
179 // Add click event
180 metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, {
181 targetType: "button",
182 id: context.pageId,
183 actionType: "share",
184 pageInformation: context.metricsPageInformation,
185 locationTracker: context.metricsLocationTracker,
186 });
187 const isLastModuleFullWidth = isArticleShelfFullWidth(objectGraph, lastShelf, context.module);
188 const shareButtonShelf = createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth);
189 if (shareButtonShelf) {
190 shelves.push(shareButtonShelf);
191 }
192 }
193 const page = new models.ArticlePage(todayCard, shelves, shareAction);
194 page.editorialStoryCard = editorialStoryCard;
195 page.title = todayCard === null || todayCard === void 0 ? void 0 : todayCard.title;
196 page.subtitle = todayCard === null || todayCard === void 0 ? void 0 : todayCard.inlineDescription;
197 addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context);
198 if (objectGraph.client.isTV) {
199 if (context.hasFocusableElements && !context.hasNonFocusableElements) {
200 page.touchMode = "focus";
201 }
202 else if (!context.hasFocusableElements && context.hasNonFocusableElements) {
203 page.touchMode = "pan";
204 }
205 else {
206 page.touchMode = "auto";
207 }
208 }
209 // Map whether the article should terminate on close.
210 page.shouldTerminateOnClose = context.isResilientDeepLink;
211 metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, context.metricsPageInformation, (fields) => {
212 let additionalValue = title;
213 if ((todayCard === null || todayCard === void 0 ? void 0 : todayCard.media) instanceof models.TodayCardMediaBrandedSingleApp &&
214 (todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay) instanceof models.TodayCardLockupOverlay) {
215 const lockupOverlay = todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay;
216 additionalValue = lockupOverlay.lockup.title;
217 }
218 if (!additionalValue) {
219 return;
220 }
221 let pageDetails = serverData.asString(serverData.asJSONValue(fields["pageDetails"]), "coercible");
222 pageDetails = pageDetails || serverData.asString(serverData.asJSONValue(fields["pageId"]));
223 if (pageDetails) {
224 fields["pageDetails"] = `${pageDetails}_${additionalValue}`;
225 }
226 else {
227 fields["pageDetails"] = `unknown_${additionalValue}`;
228 }
229 });
230 page.canonicalURL = mediaAttributes.attributeAsString(articleData, "url");
231 if (isSome(articleData)) {
232 const articleUrl = mediaAttributes.attributeAsString(articleData, "url");
233 if (isSome(articleUrl)) {
234 page.viewArticleAction = new models.ExternalUrlAction(articleUrl, true);
235 }
236 }
237 return page;
238 });
239}
240function renderArticle(objectGraph, articleData, cardMedia, context) {
241 return validation.context("renderArticle", () => {
242 var _a;
243 const shelves = [];
244 const canvas = (_a = mediaRelationships.relationshipCollection(articleData, "canvas")) !== null && _a !== void 0 ? _a : [];
245 for (const storyModule of canvas) {
246 context.module = mediaAttributes.attributeAsString(storyModule, "displayType");
247 context.subStyle = null;
248 const shelfIndex = shelves.length;
249 const shelvesToRender = renderModule(objectGraph, storyModule, articleData, context, shelfIndex);
250 if (shelvesToRender.length > 0) {
251 for (const shelf of shelvesToRender) {
252 shelf.title = context.titleForNextShelf;
253 if (objectGraph.client.isTV) {
254 // Skip unsupported tvOS shelves
255 if (shelf.contentType === "editorialLink") {
256 continue;
257 }
258 }
259 else if (objectGraph.client.isWatch) {
260 // Skip unsupported watchOS shelves
261 if (shelf.contentType === "editorialLink") {
262 continue;
263 }
264 }
265 shelves.push(shelf);
266 context.titleForNextShelf = null;
267 }
268 }
269 context.index++;
270 metricsHelpersLocation.nextPosition(context.metricsLocationTracker);
271 }
272 // If we we're showing the fallback list card type on the today page, we're going to show
273 // the lockups as a list shelf underneath so we can still display the list contents.
274 // If we're on watchOS, we also want to hit this codepath so that lockup lists do not
275 // show as empty pages.
276 if ((context.showingFallbackMediaInline ||
277 objectGraph.client.isWatch ||
278 objectGraph.client.isVision ||
279 objectGraph.client.isWeb ||
280 preprocessor.GAMES_TARGET) &&
281 shelves.length === 0) {
282 const fallbackShelf = createFallbackListShelf(objectGraph, cardMedia);
283 if (serverData.isDefinedNonNull(fallbackShelf)) {
284 shelves.push(fallbackShelf);
285 }
286 }
287 return shelves;
288 });
289}
290// region Data Augmenting
291/**
292 * Article specific entrypoint for page response augmenting. See `augment.ts`.
293 * @param response Response to augment.
294 */
295export async function fetchAdditionalDataForInitialResponse(objectGraph, response) {
296 return await mediaAugment.fetchAugmentedData(objectGraph, response, findAdditionalDataKeysForArticleResponse, fetchDataForArticleDataKey);
297}
298/**
299 * Determine the set of data, expressed as an set of `ArticleAdditionalDataKey`s, that need to be fetched for given article response to be displayed.
300 * This is equivalent to `AbstractMediaApiPageBuilder.additionalDataKeysNeededForData`, but for article builder which doesn't adopt the `builder` API.
301 *
302 * @param articleResponse Initial response to determine additional data requirements for.
303 * @returns {Set<ArticleAdditionalDataKey>} Additional data needed expressed as set of `ArticleAdditionalDataKey`
304 */
305function findAdditionalDataKeysForArticleResponse(objectGraph, articleResponse) {
306 /**
307 * Keys for requested requirements determined by:
308 * - Modules in canvas only now :)
309 */
310 const allAdditionalDataKeySet = new Set();
311 // Requirements based on canvas items:
312 const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse);
313 const canvasModules = mediaRelationships.relationshipCollection(articleData, "canvas");
314 for (const storyModule of canvasModules) {
315 // Determine additional requests and add to `allRequirementsSet`
316 const dataKeysForModule = additionalDataKeysForArticleModule(objectGraph, storyModule, articleData);
317 if (serverData.isDefinedNonNullNonEmpty(dataKeysForModule)) {
318 for (const requirement of dataKeysForModule) {
319 allAdditionalDataKeySet.add(requirement);
320 }
321 }
322 }
323 return allAdditionalDataKeySet;
324}
325/**
326 * Builds a promise that will fetch data fulfilling given requirement. Note that these promises will return `null` when they fail,
327 * and their failure should not cause the entire page to fail.
328 * This is equivalent to `AbstractMediaApiPageBuilder.fetchAdditionalDataForKey`, but for article builder which doesn't adopt the `builder` API.
329 *
330 * @param dataKey Corresponding data key to fetch data for.
331 */
332// eslint-disable-next-line @typescript-eslint/promise-function-async
333function fetchDataForArticleDataKey(objectGraph, dataKey) {
334 let request;
335 if (dataKey === "upsellForNonacquisitionCanvas") {
336 // Use `editorialItem` matching context of that would've otherwise been joined if this story was an acquisition story.
337 request = arcadeCommon.arcadeUpsellRequest(objectGraph, models.marketingItemContextFromString("editorialItemCanvas"));
338 }
339 if (dataKey === "arcadeIcons") {
340 // Require 10 for now.
341 request = arcadeCommon.arcadeAppsRequestForIcons(objectGraph, 10);
342 }
343 if (serverData.isNull(request)) {
344 return null;
345 }
346 // Failable data fetch, either resolving to valid response or `null`.
347 return mediaNetwork.fetchData(objectGraph, request).catch(() => null);
348}
349/**
350 * Determine the requirements for single article module as determined by it's type.
351 * @param storyModule The module to fetch additional requirements for.
352 * @param articleData The article that contains `storyModule` in its canvas.
353 * @returns {ArticleAdditionalDataKey[] | undefined} Set of data keys if any are needed for rendering given module.
354 */
355export function additionalDataKeysForArticleModule(objectGraph, storyModule, articleData) {
356 // Only `AppMarker` has additional requirements.
357 const moduleType = mediaAttributes.attributeAsString(storyModule, "displayType");
358 if (moduleType !== "AppMarker") {
359 return null;
360 }
361 const markerType = mediaAttributes.attributeAsString(storyModule, "appMarkerType");
362 // <rdar://problem/55919205> In story Arcade acquisition module dropping from stories
363 // 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.
364 // When an article is missing the upsell relationship, we'll fetch it separately if we have modules that need it...
365 const articleDataIsMissingUpsell = serverData.isNull(arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData));
366 /**
367 * Acquisition AppMarker, i.e. `ArcadeShowcase` needs:
368 * 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.
369 * 2. Assortment of Arcade App Icons (iOS Only).
370 */
371 const additionalDataKeysForModule = [];
372 if (markerType === "Acquisition") {
373 // iOS needs icon dependency
374 if (objectGraph.host.isiOS || objectGraph.client.isVision) {
375 additionalDataKeysForModule.push("arcadeIcons");
376 }
377 // All platform needs upsell to render acquisition modules, add it if missing.
378 if (articleDataIsMissingUpsell) {
379 additionalDataKeysForModule.push("upsellForNonacquisitionCanvas");
380 }
381 }
382 return additionalDataKeysForModule;
383}
384// endregion
385/**
386 * Create a shelf model representing a single module within article pages.
387 * @param storyModule Module server data to build shelf and contents from.
388 * @param articleData The data for article that contains `storyModule` above.
389 * @param context Global parse context updated while entire sets of modules are being parsed.
390 * @returns an array of `Shelf` or `null` if building fails for given module.
391 */
392function renderModule(objectGraph, storyModule, articleData, context, shelfIndex) {
393 return validation.catchingContext(`module: ${context.module}`, () => {
394 var _a;
395 const shelves = [];
396 switch (context.module) {
397 case "Header": {
398 context.titleForNextShelf = mediaAttributes.attributeAsString(storyModule, "editorialCopy");
399 break;
400 }
401 case "TextBlock": {
402 const textBlockShelf = createParagraph(objectGraph, storyModule, context);
403 if (isSome(textBlockShelf)) {
404 shelves.push(textBlockShelf);
405 context.hasNonFocusableElements = true;
406 }
407 break;
408 }
409 case "CollectionLockup": {
410 const appListShelf = createAppList(objectGraph, storyModule, context);
411 if (isSome(appListShelf)) {
412 shelves.push(appListShelf);
413 context.hasFocusableElements = true;
414 }
415 break;
416 }
417 case "InlineImage": {
418 const inlineImageShelf = createImage(objectGraph, storyModule, context);
419 if (isSome(inlineImageShelf)) {
420 shelves.push(inlineImageShelf);
421 context.hasNonFocusableElements = true;
422 }
423 break;
424 }
425 case "AppLockup": {
426 const appLockupShelf = createAppLockup(objectGraph, storyModule, context);
427 if (isSome(appLockupShelf)) {
428 shelves.push(appLockupShelf);
429 context.hasFocusableElements = true;
430 }
431 break;
432 }
433 case "TipBlock": {
434 const tipShelf = createTip(objectGraph, storyModule, context);
435 if (isSome(tipShelf)) {
436 shelves.push(tipShelf);
437 context.hasNonFocusableElements = true;
438 }
439 break;
440 }
441 case "PullQuote": {
442 const pullQuoteShelf = createPullQuote(objectGraph, storyModule, context);
443 if (isSome(pullQuoteShelf)) {
444 shelves.push(pullQuoteShelf);
445 context.hasNonFocusableElements = true;
446 }
447 break;
448 }
449 case "HorizontalRule": {
450 const horizontalRuleShelf = createHorizontalRule(objectGraph, storyModule, context);
451 if (isSome(horizontalRuleShelf)) {
452 shelves.push(horizontalRuleShelf);
453 context.hasNonFocusableElements = true;
454 }
455 break;
456 }
457 case "InlineVideo": {
458 const inlineVideoShelf = createVideo(objectGraph, storyModule, context);
459 if (isSome(inlineVideoShelf)) {
460 shelves.push(inlineVideoShelf);
461 context.hasFocusableElements = true;
462 }
463 break;
464 }
465 case "AppMedia": {
466 const appMediaShelf = createAppMedia(objectGraph, storyModule, context);
467 if (isSome(appMediaShelf)) {
468 shelves.push(appMediaShelf);
469 context.hasFocusableElements = true;
470 }
471 break;
472 }
473 case "LinkBlock": {
474 const linkBlockShelf = createLink(objectGraph, storyModule, context);
475 if (isSome(linkBlockShelf)) {
476 shelves.push(linkBlockShelf);
477 context.hasFocusableElements = true;
478 }
479 break;
480 }
481 case "TextList": {
482 const textListShelf = createTextList(objectGraph, storyModule, context);
483 if (isSome(textListShelf)) {
484 shelves.push(textListShelf);
485 context.hasNonFocusableElements = true;
486 }
487 break;
488 }
489 case "IAPLockup": {
490 const iapLockupShelf = createIAPLockup(objectGraph, storyModule, context);
491 if (isSome(iapLockupShelf)) {
492 shelves.push(iapLockupShelf);
493 context.hasFocusableElements = true;
494 }
495 break;
496 }
497 case "AppMarker": {
498 const appMarkerShelf = createAppMarker(objectGraph, storyModule, articleData, context);
499 if (isSome(appMarkerShelf)) {
500 shelves.push(appMarkerShelf);
501 context.hasFocusableElements = true;
502 }
503 break;
504 }
505 case "StoryList": {
506 const storyListShelf = createStoryCards(objectGraph, storyModule, context, shelfIndex);
507 if (isSome(storyListShelf)) {
508 shelves.push(storyListShelf);
509 context.hasFocusableElements = true;
510 }
511 break;
512 }
513 case "AppEventLockup": {
514 const appEventShelf = createAppEventLockup(objectGraph, storyModule, context);
515 if (isSome(appEventShelf)) {
516 shelves.push(appEventShelf);
517 context.hasFocusableElements = true;
518 }
519 break;
520 }
521 case "OfferItemLockup": {
522 const offerItemShelves = createOfferItemLockup(objectGraph, storyModule, context);
523 if (isSome(offerItemShelves)) {
524 shelves.push(...offerItemShelves);
525 context.hasFocusableElements = true;
526 }
527 break;
528 }
529 default: {
530 objectGraph.console.log(`Unknown module: ${context.module}`);
531 }
532 }
533 for (const shelf of shelves) {
534 const existingShelfPresentationHints = (_a = shelf.presentationHints) !== null && _a !== void 0 ? _a : {};
535 shelf.presentationHints = {
536 ...existingShelfPresentationHints,
537 isArticleContext: true,
538 };
539 }
540 return shelves;
541 });
542}
543const FULL_WIDTH_MODULES = ["AppLockup", "InlineImage", "InlineVideo", "AppMarker"];
544/**
545 * Determines whether the provided parameters signifies a full-width article
546 * module.
547 * @param shelf The shelf in question.
548 * @param type The type of article module.
549 * @returns Whether or not the given shelf for the article type is full width.
550 */
551function isArticleShelfFullWidth(objectGraph, shelf, type) {
552 if (shelf && type) {
553 const itemCount = shelf.items.length;
554 if (itemCount > 0 && FULL_WIDTH_MODULES.indexOf(type) !== -1) {
555 const lastItem = shelf.items[itemCount - 1];
556 switch (shelf.contentType) {
557 case "framedArtwork": {
558 const framedArt = lastItem;
559 return framedArt && framedArt.isFullWidth;
560 }
561 case "framedVideo": {
562 const framedVideo = lastItem;
563 return framedVideo && framedVideo.isFullWidth;
564 }
565 default: {
566 return true;
567 }
568 }
569 }
570 }
571 return false;
572}
573// region Footer Lockup
574/**
575 * Adds either a `footerLockup` or `arcadeFooterLockup` property on `ArticlePage` model, based on type of article.
576 * @param page Page to add footer to if needed.
577 * @param articleData Original data of article being rendered.
578 * @param context Parse context for article builder.
579 */
580function addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context) {
581 // App Lockup for Articles about single specific app.
582 const footerProductData = productDataFromArticle(objectGraph, articleData);
583 if (footerProductData) {
584 const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, articleData);
585 page.footerLockup = productFooterLockupFromData(objectGraph, footerProductData, context, externalDeepLinkUrl);
586 return;
587 }
588 // Arcade Lockup for Acquisition Story for supported platforms
589 const isArcadeAcquisitionEI = mediaAttributes.attributeAsBooleanOrFalse(articleData, "isAcquisition");
590 const platformSupportsArcadeFooterLockup = objectGraph.host.isiOS || objectGraph.host.isMac;
591 const additionalDataIsAvailable = serverData.isDefinedNonNull(context.additionalData);
592 if (additionalDataIsAvailable && isArcadeAcquisitionEI && platformSupportsArcadeFooterLockup) {
593 const upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData);
594 page.arcadeFooterLockup = arcadeFooterLockupFromData(objectGraph, upsellData, context);
595 }
596}
597/**
598 * Find platform data from editorial item to enhance sharing and display in footer lockup
599 * At the moment, only single app editorials get footer lockups and have enhanced sharing.
600 *
601 * @param editorialItem Item to find footer content for
602 * @returns content to display in footer lockup, or null if no content should be displayed
603 */
604export function productDataFromArticle(objectGraph, editorialItem) {
605 const relatedContent = mediaRelationships.relationshipCollection(editorialItem, "card-contents");
606 if (relatedContent.length !== 1) {
607 return null;
608 }
609 const contentData = relatedContent[0];
610 if (!contentData) {
611 return null;
612 }
613 switch (contentData.type) {
614 case "apps":
615 case "app-bundles":
616 return contentData;
617 default:
618 return null;
619 }
620}
621/**
622 * Creates a footer lockup with a data for a specific app.
623 * Cover method over `lockupFromData` to override `offerStyle`.
624 *
625 * @param data MAPI data to build footer with.
626 * @param context Parse context
627 * @param externalDeepLinkUrl promotional deep link url to use on the lockup's offer.
628 * @returns A new `Lockup` object for footer lockups.
629 */
630function productFooterLockupFromData(objectGraph, data, context, externalDeepLinkUrl) {
631 const lockupOptions = {
632 offerStyle: footerLockupOfferStyle(objectGraph),
633 metricsOptions: {
634 pageInformation: context.metricsPageInformation,
635 locationTracker: context.metricsLocationTracker,
636 },
637 clientIdentifierOverride: context.clientIdentifierOverride,
638 externalDeepLinkUrl: externalDeepLinkUrl,
639 crossLinkSubtitle: context.crossLinkSubtitle,
640 artworkUseCase: 0 /* content.ArtworkUseCase.Default */,
641 canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "smallLockup"),
642 };
643 return lockups.lockupFromData(objectGraph, data, lockupOptions);
644}
645/**
646 * Creates a footer lockup representing the Arcade subscription service.
647 * @param upsellData Contains both editorial and iAP data for Arcade
648 * @param context Parse context.
649 */
650function arcadeFooterLockupFromData(objectGraph, upsellData, context) {
651 const metricsOptions = {
652 pageInformation: context.metricsPageInformation,
653 locationTracker: context.metricsLocationTracker,
654 };
655 return lockups.arcadeLockupFromData(objectGraph, upsellData, metricsOptions, models.marketingItemContextFromString("editorialItem"), "infer", null);
656}
657/**
658 * Determines the offer style to use for the footer lockup.
659 */
660function footerLockupOfferStyle(objectGraph) {
661 switch (objectGraph.client.deviceType) {
662 case "mac":
663 return "white";
664 default:
665 return "infer";
666 }
667}
668// endregion
669function createFallbackListShelf(objectGraph, cardMedia) {
670 if (cardMedia instanceof models.TodayCardMediaList || cardMedia instanceof models.TodayCardMediaRiver) {
671 const fallbackShelf = new models.Shelf("smallLockup");
672 fallbackShelf.items = cardMedia.lockups;
673 if (objectGraph.client.isWeb) {
674 fallbackShelf.presentationHints = {
675 ...fallbackShelf.presentationHints,
676 isArticleContext: true,
677 };
678 }
679 return fallbackShelf;
680 }
681 return null;
682}
683function shareSheetActionFromData(objectGraph, editorialItem, todayCardConfig) {
684 const productData = productDataFromArticle(objectGraph, editorialItem);
685 /*
686 * Determine title
687 */
688 let title = null;
689 const name = content.notesFromData(objectGraph, editorialItem, "name");
690 const short = content.notesFromData(objectGraph, editorialItem, "short");
691 // Prefer "name: short"
692 if (name && short) {
693 title = objectGraph.loc
694 .string("ShareSheet.TitleSubtitle.Format", "{title}: {subtitle}")
695 .replace("{title}", name)
696 .replace("{subtitle}", short);
697 }
698 // Followed by name
699 if (!title && name) {
700 title = name;
701 }
702 // Followed by short
703 if (!title && short) {
704 title = short;
705 }
706 // Followed by product name
707 if (!title && productData) {
708 const productTitle = mediaAttributes.attributeAsString(productData, "name");
709 const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle");
710 switch (cardDisplayStyle) {
711 case TodayCardDisplayStyle.GameOfTheDay: {
712 title = objectGraph.loc.string("SHARE_SHEET_GAME_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle);
713 break;
714 }
715 case TodayCardDisplayStyle.AppOfTheDay: {
716 title = objectGraph.loc.string("SHARE_SHEET_APP_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle);
717 break;
718 }
719 default: {
720 objectGraph.console.log(`No title for article with unknown style: ${cardDisplayStyle}`);
721 break;
722 }
723 }
724 }
725 const url = mediaAttributes.attributeAsString(editorialItem, "url");
726 let articleArtwork;
727 const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle");
728 switch (cardDisplayStyle) {
729 case TodayCardDisplayStyle.Grid:
730 case TodayCardDisplayStyle.List:
731 case TodayCardDisplayStyle.River:
732 articleArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://ShareCollectionThumbnail", 40, 40);
733 break;
734 default:
735 articleArtwork = null;
736 break;
737 }
738 // Create share sheet model (bail out if unable to do so)
739 const shareData = sharing.shareSheetDataForArticle(objectGraph, title, url, null, articleArtwork, editorialItem);
740 if (!serverData.isDefinedNonNull(shareData)) {
741 return null;
742 }
743 const activities = sharing.shareSheetActivitiesForArticle(objectGraph, url, todayCardPreviewUrlForTodayCard(objectGraph, editorialItem.id, todayCardConfig), editorialItem.id);
744 return new models.ShareSheetAction(shareData, activities);
745}
746function createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth) {
747 if (!serverData.isDefinedNonNull(shareAction) ||
748 objectGraph.client.isVision ||
749 preprocessor.GAMES_TARGET ||
750 objectGraph.client.isCompanionVisionApp) {
751 return null;
752 }
753 // Create share button
754 const shareButton = new models.RoundedButton("share", objectGraph.loc.string("SHARE_STORY"), !isLastModuleFullWidth, shareAction);
755 // Add share shelf
756 const shareButtonShelf = new models.Shelf("roundedButton");
757 shareButtonShelf.items = [shareButton];
758 return shareButtonShelf;
759}
760function createParagraph(objectGraph, module, context) {
761 const text = mediaAttributes.attributeAsString(module, "editorialCopy");
762 if (!text) {
763 return null;
764 }
765 const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article");
766 // Setup impressions
767 addImpressionsFieldsToModel(objectGraph, paragraph, context);
768 const shelf = new models.Shelf("paragraph");
769 shelf.items = [paragraph];
770 return shelf;
771}
772function createImage(objectGraph, module, context) {
773 const displayStyle = mediaAttributes.attributeAsString(module, "inlineImageDisplayType");
774 const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork");
775 // If the displayStyle is FullWidth want to 'allowTransparency' so that images blend into the page in both
776 // light and dark mode. Previously editorial would bake white backgrounds into images they wanted to 'blend'
777 // with the page
778 const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, {
779 useCase: 13 /* content.ArtworkUseCase.ArticleImage */,
780 allowingTransparency: displayStyle === "FullWidth" && !objectGraph.client.isVision,
781 withJoeColorPlaceholder: objectGraph.client.isVision,
782 });
783 if (!artwork) {
784 return null;
785 }
786 const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml");
787 // Get the optional caption
788 frame.caption = mediaAttributes.attributeAsString(module, "editorialCopy");
789 context.subStyle = displayStyle;
790 if (displayStyle) {
791 switch (displayStyle) {
792 case "BoundingBox": {
793 frame.isFullWidth = false;
794 frame.hasRoundedCorners = true;
795 break;
796 }
797 case "FullWidth":
798 default: {
799 frame.isFullWidth = true;
800 frame.hasRoundedCorners = false;
801 break;
802 }
803 }
804 }
805 // Setup impressions
806 addImpressionsFieldsToModel(objectGraph, frame, context);
807 const shelf = new models.Shelf("framedArtwork");
808 shelf.items = [frame];
809 return shelf;
810}
811function createTip(objectGraph, module, context) {
812 const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork");
813 const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, {
814 useCase: 13 /* content.ArtworkUseCase.ArticleImage */,
815 });
816 if (!artwork) {
817 return null;
818 }
819 const caption = mediaAttributes.attributeAsString(module, "editorialCopy");
820 const ordinal = mediaAttributes.attributeAsString(module, "tipNumber");
821 // Create the tip image
822 const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml");
823 frame.isFullWidth = false;
824 frame.hasRoundedCorners = true;
825 frame.caption = caption;
826 frame.ordinal = ordinal;
827 // Setup impressions
828 addImpressionsFieldsToModel(objectGraph, frame, context);
829 // Create the shelf
830 const shelf = new models.Shelf("framedArtwork");
831 shelf.items = [frame];
832 return shelf;
833}
834function createPullQuote(objectGraph, module, context) {
835 const text = mediaAttributes.attributeAsString(module, "quote");
836 const attribution = mediaAttributes.attributeAsString(module, "quoteAttribution");
837 // Get the optional artwork
838 const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork");
839 const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, {
840 useCase: 13 /* content.ArtworkUseCase.ArticleImage */,
841 });
842 const fullWidth = mediaAttributes.attributeAsString(module, "pullQuoteDisplayType") === "FullWidth";
843 // Create the quote
844 const quote = new models.Quote(text, attribution, artwork, fullWidth);
845 // Setup impressions
846 addImpressionsFieldsToModel(objectGraph, quote, context);
847 // Create the shelf
848 const shelf = new models.Shelf("quote");
849 shelf.items = [quote];
850 return shelf;
851}
852function createHorizontalRule(objectGraph, module, context) {
853 const lineStyle = mediaAttributes.attributeAsString(module, "lineStyle");
854 const fullWidth = mediaAttributes.attributeAsString(module, "displayStyle") === "FullWidth";
855 let ruleColor = color.named("defaultLine");
856 if (objectGraph.client.isVision && (lineStyle === "Dotted" || lineStyle === "Dashed")) {
857 ruleColor = color.white;
858 }
859 // Parse the customColor from Media API. This can only be a dynamic color.
860 const apiColor = mediaAttributes.attributeAsDictionary(module, "customColor");
861 const lightColor = color.fromHex(serverData.asString(apiColor, "lightMode"));
862 const darkColor = color.fromHex(serverData.asString(apiColor, "darkMode"));
863 if (!serverData.isNullOrEmpty(lightColor) && !serverData.isNullOrEmpty(darkColor)) {
864 ruleColor = color.dynamicWith(lightColor, darkColor);
865 }
866 const horizontalRule = new models.HorizontalRule(lineStyle, ruleColor, fullWidth);
867 // Create the Shelf
868 const shelf = new models.Shelf("horizontalRule");
869 shelf.items = [horizontalRule];
870 return shelf;
871}
872function createVideo(objectGraph, module, context) {
873 // Get the preview artwork
874 const artworkData = mediaAttributes.attributeAsDictionary(module, "video.previewFrame");
875 const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, {
876 useCase: 13 /* content.ArtworkUseCase.ArticleImage */,
877 });
878 if (!artwork) {
879 return null;
880 }
881 // Get the video URL
882 const videoUrl = mediaAttributes.attributeAsString(module, "video.video");
883 if (!videoUrl) {
884 return null;
885 }
886 const videoDisplayType = mediaAttributes.attributeAsString(module, "inlineVideoDisplayType");
887 const isFullWidth = videoDisplayType === "FullWidth";
888 // Create the video
889 const video = new models.Video(videoUrl, artwork, videoDefaults.defaultVideoConfiguration(objectGraph));
890 metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, {
891 pageInformation: context.metricsPageInformation,
892 locationTracker: context.metricsLocationTracker,
893 id: context.pageId,
894 });
895 const videoModule = new models.FramedVideo(video, isFullWidth, "text/x-apple-as3-nqml");
896 // Get the optional caption
897 videoModule.caption = mediaAttributes.attributeAsString(module, "editorialCopy");
898 // Setup impressions
899 addImpressionsFieldsToModel(objectGraph, videoModule, context);
900 // Create the shelf
901 const shelf = new models.Shelf("framedVideo");
902 shelf.items = [videoModule];
903 return shelf;
904}
905function createAppLockup(objectGraph, module, context) {
906 const contentData = contentFromModule(objectGraph, module, context);
907 if (!contentData) {
908 return null;
909 }
910 // Shelf to generate. Either lockup, app showcase, or app event shelf
911 let shelf = null;
912 // If we have an app-events relationship, we want to use this as the priority. This sometimes exists on the
913 // AppLockup type, rather than as the AppEventLockup type, so that older clients can still render this
914 // item by falling back to the AppLockup type.
915 const appEventsDataItems = mediaRelationships.relationshipCollection(module, "app-events");
916 if (serverData.isDefinedNonNullNonEmpty(appEventsDataItems)) {
917 shelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, appEventsDataItems, context.metricsPageInformation, context.metricsLocationTracker, context);
918 if (serverData.isDefinedNonNull(shelf)) {
919 return shelf;
920 }
921 }
922 // Set the display style
923 const displayStyle = mediaAttributes.attributeAsString(module, "appLockupSize");
924 context.subStyle = displayStyle;
925 let shelfStyle;
926 let isLockup = false;
927 if (displayStyle) {
928 switch (displayStyle) {
929 case "Small": {
930 shelfStyle = "smallLockup";
931 isLockup = true;
932 break;
933 }
934 case "Medium": {
935 shelfStyle = "mediumLockup";
936 isLockup = true;
937 break;
938 }
939 case "Large":
940 default: {
941 if (objectGraph.client.isWatch ||
942 objectGraph.client.isTV ||
943 objectGraph.client.isVision ||
944 preprocessor.GAMES_TARGET) {
945 // Per design, on watchOS we always show a lockup for app showcases.
946 // Watch App Store treats all lockup sizes the same -- let's pick small.
947 shelfStyle = "smallLockup";
948 isLockup = true;
949 }
950 else {
951 shelfStyle = "appShowcase";
952 }
953 break;
954 }
955 }
956 }
957 // Determine the deep link URL, if there is one.
958 const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module);
959 // Create the appropriate shelf item
960 if (isLockup) {
961 const lockupShelf = new models.Shelf(shelfStyle);
962 const metricsOptions = {
963 metricsOptions: {
964 pageInformation: context.metricsPageInformation,
965 locationTracker: context.metricsLocationTracker,
966 },
967 clientIdentifierOverride: context.clientIdentifierOverride,
968 externalDeepLinkUrl: externalDeepLinkUrl,
969 artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfStyle),
970 };
971 let lockup;
972 if (preprocessor.GAMES_TARGET) {
973 const shelfID = new PageID(context.pageId).shelfID(module.id);
974 lockup = gamesComponentBuilder.makeArticleGameLockup(objectGraph, contentData, shelfID);
975 }
976 else {
977 lockup = lockups.lockupFromData(objectGraph, contentData, metricsOptions);
978 }
979 if (isNothing(lockup)) {
980 return null;
981 }
982 lockupShelf.items = [lockup];
983 shelf = lockupShelf;
984 }
985 else {
986 // On all platforms, the AppLockup platform generates a AppShowcase when display style is large.
987 shelf = createAppShowcase(objectGraph, module, context);
988 }
989 return shelf;
990}
991function createAppShowcase(objectGraph, module, context) {
992 // Create the shelf
993 const shelf = new models.Shelf("appShowcase");
994 // Parameterize by platform:
995 // tvOS populates the `screenshots` field to display alongside video.
996 const showcaseHasScreenshots = objectGraph.client.isTV;
997 // Only non-tvOS has shelf background color
998 const shelfHasBackgroundColor = objectGraph.client.deviceType !== "tv";
999 const contentData = contentFromModule(objectGraph, module, context);
1000 const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module);
1001 const lockup = lockups.lockupFromData(objectGraph, contentData, {
1002 offerStyle: "colored",
1003 metricsOptions: {
1004 pageInformation: context.metricsPageInformation,
1005 locationTracker: context.metricsLocationTracker,
1006 },
1007 clientIdentifierOverride: context.clientIdentifierOverride,
1008 externalDeepLinkUrl: externalDeepLinkUrl,
1009 crossLinkSubtitle: context.crossLinkSubtitle,
1010 artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */,
1011 });
1012 const showcase = new models.AppShowcase("large", lockup);
1013 showcase.description = lockups.subtitleFromData(objectGraph, contentData);
1014 // Add Video
1015 // Configure the video for the showcase, if the module demands it.
1016 let showcaseVideo = null;
1017 const videoType = mediaAttributes.attributeAsString(module, "appLockupVideo");
1018 switch (videoType) {
1019 case "AppTrailer": {
1020 const allAppVideos = content.videoPreviewsFromData(objectGraph, contentData);
1021 if (allAppVideos && allAppVideos.length > 0) {
1022 showcaseVideo = allAppVideos[0];
1023 }
1024 break;
1025 }
1026 default:
1027 break;
1028 }
1029 if (showcaseVideo) {
1030 metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, showcaseVideo, {
1031 pageInformation: context.metricsPageInformation,
1032 locationTracker: context.metricsLocationTracker,
1033 id: context.pageId,
1034 });
1035 showcase.video = showcaseVideo;
1036 }
1037 // Add Screenshots for AppShowcase if necessary
1038 if (showcaseHasScreenshots) {
1039 showcase.screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [content.currentAppPlatform(objectGraph)]);
1040 }
1041 // Configure background if necessary.
1042 if (shelfHasBackgroundColor) {
1043 shelf.background = {
1044 type: "color",
1045 color: appShowcaseBackgroundColor,
1046 };
1047 }
1048 shelf.items = [showcase];
1049 return shelf;
1050}
1051function createIAPLockup(objectGraph, module, context) {
1052 const contentData = contentFromModule(objectGraph, module, context);
1053 if (!contentData) {
1054 return null;
1055 }
1056 // Create the lockup
1057 const lockup = lockups.inAppPurchaseLockupFromData(objectGraph, contentData, {
1058 metricsOptions: {
1059 pageInformation: context.metricsPageInformation,
1060 locationTracker: context.metricsLocationTracker,
1061 },
1062 clientIdentifierOverride: context.clientIdentifierOverride,
1063 artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */,
1064 });
1065 if (!lockup) {
1066 return null;
1067 }
1068 const showcase = new models.InAppPurchaseShowcase(lockup);
1069 // Create the shelf
1070 const shelf = new models.Shelf("inAppPurchaseShowcase");
1071 shelf.background = {
1072 type: "color",
1073 color: iAPBackgroundColor,
1074 };
1075 shelf.items = [showcase];
1076 return shelf;
1077}
1078function createAppList(objectGraph, module, context) {
1079 const showOrdinals = mediaAttributes.attributeAsBooleanOrFalse(module, "showOrdinals");
1080 const ordinalDirection = mediaAttributes.attributeAsString(module, "collectionLockupDisplayType") === "OrdinalDesc"
1081 ? "descending"
1082 : "ascending";
1083 // Set the display style
1084 const displayStyle = mediaAttributes.attributeAsString(module, "collectionLockupSize");
1085 context.subStyle = displayStyle;
1086 let style;
1087 if (displayStyle) {
1088 switch (displayStyle) {
1089 case "Large": {
1090 style = "largeLockup";
1091 break;
1092 }
1093 case "Medium": {
1094 style = "mediumLockup";
1095 break;
1096 }
1097 case "Small":
1098 default: {
1099 style = "smallLockup";
1100 break;
1101 }
1102 }
1103 }
1104 // Construct the lockup options
1105 const lockupOptions = {
1106 metricsOptions: {
1107 pageInformation: context.metricsPageInformation,
1108 locationTracker: context.metricsLocationTracker,
1109 },
1110 clientIdentifierOverride: context.clientIdentifierOverride,
1111 artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, style),
1112 canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, style),
1113 };
1114 // Check if we have content
1115 const contents = mediaRelationships.relationshipCollection(module, "contents");
1116 if (isNothing(contents)) {
1117 return null;
1118 }
1119 let childLockups = [];
1120 if (preprocessor.GAMES_TARGET) {
1121 const shelfID = new PageID(context.pageId).shelfID(module.id);
1122 childLockups = gamesComponentBuilder.makeArticleGameLockups(objectGraph, contents, shelfID);
1123 }
1124 else {
1125 childLockups = lockups.lockupsFromData(objectGraph, contents, {
1126 includeOrdinals: showOrdinals,
1127 ordinalDirection: ordinalDirection,
1128 lockupOptions: lockupOptions,
1129 });
1130 }
1131 if (!childLockups || childLockups.length === 0) {
1132 return null;
1133 }
1134 // Create the shelf
1135 const shelf = new models.Shelf(style);
1136 shelf.items = childLockups;
1137 return shelf;
1138}
1139function createAppMedia(objectGraph, module, context) {
1140 const contentData = contentFromModule(objectGraph, module, context);
1141 if (!contentData) {
1142 return null;
1143 }
1144 // Set the display style
1145 const mediaOption = mediaAttributes.attributeAsString(module, "appMediaOption");
1146 const appMediaPlatform = mediaAttributes.attributeAsString(module, "appMediaPlatform");
1147 context.subStyle = mediaOption;
1148 switch (mediaOption) {
1149 case "Screenshots": {
1150 let shelf = null;
1151 // I'm so sorry, but making this split makes the macOS client code infinitely better because we are able
1152 // to reuse the same product media view and component contract that is used on product page screenshots/trailers.
1153 // Really, iOS should be reworked such that its module & product page implementation has a single source,
1154 // but this has serious design obstacles that need to be worked through.
1155 if (objectGraph.client.isMac) {
1156 shelf = new models.Shelf("productMedia");
1157 const productMedia = content.productMediaFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */);
1158 if (serverData.isDefinedNonNull(productMedia) && productMedia.length) {
1159 shelf.items = productMedia;
1160 }
1161 }
1162 else {
1163 shelf = new models.Shelf("screenshots");
1164 if (serverData.isNull(appMediaPlatform)) {
1165 /**
1166 * The server did not tell us which app platform to use, so we need to infer based on various keys in
1167 * the response. These parameters are only fully baked into product-dv responses, so we we need to do
1168 * the more expensive product-dv lookup in order to correctly infer the default screenshots to use for
1169 * the shelf.
1170 */
1171 const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */);
1172 if (screenshots && screenshots.length > 0) {
1173 shelf.items = [screenshots[0]];
1174 }
1175 }
1176 else {
1177 /**
1178 * Server tells us which platform to use -- dictated by `appMediaPlatform`. Selectively do a lookup for
1179 * just those screenshots.
1180 */
1181 const desiredAppPlatform = appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform);
1182 if (desiredAppPlatform) {
1183 const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [desiredAppPlatform]);
1184 if (screenshots && screenshots.length) {
1185 shelf.items = [screenshots[0]];
1186 }
1187 }
1188 }
1189 }
1190 if (serverData.isDefinedNonNull(shelf) && shelf.items.length === 0) {
1191 return null;
1192 }
1193 return shelf;
1194 }
1195 case "AppTrailers":
1196 const trailersShelf = new models.Shelf("framedVideo");
1197 const videoPreviews = content.videoPreviewsFromData(objectGraph, contentData);
1198 if (videoPreviews && videoPreviews.length > 0) {
1199 const video = videoPreviews[0];
1200 metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, {
1201 pageInformation: context.metricsPageInformation,
1202 locationTracker: context.metricsLocationTracker,
1203 id: context.pageId,
1204 });
1205 const firstTrailer = new models.FramedVideo(video, false, "text/plain", null, null, true);
1206 trailersShelf.items = [firstTrailer];
1207 return trailersShelf;
1208 }
1209 else {
1210 return null;
1211 }
1212 default: {
1213 return null;
1214 }
1215 }
1216}
1217function createLink(objectGraph, module, context) {
1218 if (objectGraph.client.isTV || objectGraph.client.isWatch) {
1219 return null;
1220 }
1221 const urlString = mediaAttributes.attributeAsString(module, "url");
1222 if (!urlString) {
1223 return null;
1224 }
1225 const url = new urls.URL(urlString);
1226 const linkTitle = mediaAttributes.attributeAsString(module, "urlTitle");
1227 let text = mediaAttributes.attributeAsString(module, "editorialCopy");
1228 if (!text) {
1229 text = url.host;
1230 }
1231 const mediaHosts = [
1232 "itunes.apple.com",
1233 "apps.apple.com",
1234 "music.apple.com",
1235 "books.apple.com",
1236 "podcasts.apple.com",
1237 "watch-app.cdn-apple.com",
1238 "tv.apple.com",
1239 ];
1240 let linkPresentationEnabled = false;
1241 for (const mediaHost of mediaHosts) {
1242 if (url.host.endsWith(mediaHost)) {
1243 linkPresentationEnabled = true;
1244 }
1245 }
1246 const action = new models.ExternalUrlAction(urlString);
1247 metricsHelpersClicks.addClickEventToAction(objectGraph, action, {
1248 targetType: "link",
1249 pageInformation: context.metricsPageInformation,
1250 id: `${context.index}`,
1251 locationTracker: context.metricsLocationTracker,
1252 });
1253 const link = new models.EditorialLink(linkTitle, text, action, linkPresentationEnabled);
1254 // Setup impressions
1255 addImpressionsFieldsToModel(objectGraph, link, context);
1256 const shelf = new models.Shelf("editorialLink");
1257 shelf.items = [link];
1258 return shelf;
1259}
1260function createTextList(objectGraph, module, context) {
1261 const listEntries = mediaAttributes.attributeAsArrayOrEmpty(module, "editorialCopy");
1262 if (!listEntries.length) {
1263 return null;
1264 }
1265 const type = mediaAttributes.attributeAsString(module, "textListDisplayType");
1266 context.subStyle = type;
1267 let isBulleted = false;
1268 switch (type) {
1269 case "Bulleted": {
1270 isBulleted = true;
1271 break;
1272 }
1273 default: {
1274 isBulleted = false;
1275 break;
1276 }
1277 }
1278 let text;
1279 if (isBulleted) {
1280 text = "<ul>";
1281 }
1282 else {
1283 text = "<ol>";
1284 }
1285 for (const textEntry of listEntries) {
1286 const listItemJSONString = JSON.stringify(textEntry);
1287 // rdar://104446319 - We must use `parse` on our JSON string to convert back to
1288 // a raw string object as this ensures leading/trailing quotation marks are *not* escaped
1289 const listItem = JSON.parse(listItemJSONString);
1290 text = `${text}<li>${listItem}</li>`;
1291 }
1292 if (isBulleted) {
1293 text = `${text}</ul>`;
1294 }
1295 else {
1296 text = `${text}</ol>`;
1297 }
1298 const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article");
1299 // Setup impressions
1300 addImpressionsFieldsToModel(objectGraph, paragraph, context);
1301 const shelf = new models.Shelf("paragraph");
1302 shelf.items = [paragraph];
1303 return shelf;
1304}
1305function createStoryCards(objectGraph, module, context, shelfIndex) {
1306 if (objectGraph.client.isVision) {
1307 const shelfToken = createBaseShelfToken(objectGraph, undefined, module, false, shelfIndex, context.metricsPageInformation, context.metricsLocationTracker);
1308 const shelf = buildSmallStoryCardShelf(objectGraph, shelfToken);
1309 shelf.isHorizontal = true;
1310 return shelf;
1311 }
1312 const cards = mediaRelationships.relationshipCollection(module, "contents");
1313 if (!cards) {
1314 return null;
1315 }
1316 const title = mediaAttributes.attributeAsString(module, "name");
1317 const subtitle = mediaAttributes.attributeAsString(module, "tagline");
1318 let shelf = null;
1319 if (objectGraph.client.isiOS && objectGraph.featureFlags.isEnabled("mini_today_cards_article")) {
1320 const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker);
1321 shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, cards, title, subtitle, todayParseContext);
1322 }
1323 else {
1324 const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb;
1325 const resolvedContentType = isSmallStoryCardsSupported ? "smallStoryCard" : "todayBrick";
1326 shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, cards, resolvedContentType, title, subtitle, context, null);
1327 if (isSmallStoryCardsSupported) {
1328 // Only specific small story cards are supported and will crash otherwise, filter those here preemptively.
1329 // rdar://91965501 (MAS Crashing - Earth Day Landing Page - 4/19)
1330 if (Array.isArray(shelf.items)) {
1331 shelf.items = shelf.items.filter((item) => {
1332 if (!(item instanceof models.TodayCard)) {
1333 return true;
1334 }
1335 return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, resolvedContentType);
1336 });
1337 }
1338 }
1339 }
1340 return shelf;
1341}
1342function createAppEventLockup(objectGraph, module, context) {
1343 const contentData = contentFromModule(objectGraph, module, context);
1344 if (!contentData) {
1345 return null;
1346 }
1347 return appPromotionsShelf.appEventsShelfForArticle(objectGraph, [contentData], context.metricsPageInformation, context.metricsLocationTracker, context);
1348}
1349function createOfferItemLockup(objectGraph, module, context) {
1350 if (!objectGraph.client.isiOS) {
1351 return [];
1352 }
1353 const offerItem = mediaRelationships.relationshipData(objectGraph, module, "contents");
1354 if (serverData.isNullOrEmpty(offerItem)) {
1355 return null;
1356 }
1357 // Offer detail Paragraph
1358 const offerParagraph = mediaAttributes.attributeAsString(module, "editorialCopy");
1359 const paragraph = new models.Paragraph(offerParagraph, "text/x-apple-as3-nqml", "article");
1360 const paragraphShelf = new models.Shelf("paragraph");
1361 paragraphShelf.items = [paragraph];
1362 // Winback Offer Card
1363 const offerItemShelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, [offerItem], context.metricsPageInformation, context.metricsLocationTracker, context);
1364 return [paragraphShelf, offerItemShelf];
1365}
1366/**
1367 * Ingests EI canvas modules of form:
1368 * {
1369 * id: <editorial-id>,
1370 * type: "editorial-item-shelves",
1371 * attributes: {
1372 * displayType: "AppMarker",
1373 * appMarkerType: <AppMarkerType>
1374 * }
1375 * }
1376 * to generate an shelf for AppMarker model.
1377 */
1378function createAppMarker(objectGraph, appMarkerModule, articleData, context) {
1379 const markerType = mediaAttributes.attributeAsString(appMarkerModule, "appMarkerType");
1380 context.subStyle = markerType;
1381 let shelf = null;
1382 switch (markerType) {
1383 case "OSUpgrade":
1384 shelf = createOSUpgradeClientControlButton(objectGraph, appMarkerModule, context);
1385 break;
1386 case "Acquisition":
1387 shelf = createArcadeShowcase(objectGraph, appMarkerModule, articleData, context);
1388 break;
1389 default:
1390 break;
1391 }
1392 return shelf;
1393}
1394/**
1395 * Ingests EI canvas modules of form:
1396 * {
1397 * id: <editorial-id>,
1398 * type: "editorial-item-shelves",
1399 * attributes: {
1400 * displayType: "AppMarker",
1401 * appMarkerType: "OSUpgrade"
1402 * }
1403 * }
1404 * to generate an shelf with an button that links to preferences updates.
1405 */
1406function createOSUpgradeClientControlButton(objectGraph, osUpgradeModule, context) {
1407 const deviceType = objectGraph.client.deviceType;
1408 if (deviceType !== "mac") {
1409 return null; // Early exit - Only MAS utilizes OS Upgrade Client Control Button currently.
1410 }
1411 const installUpdateUrl = links.osUpdateUrl(deviceType);
1412 if (installUpdateUrl === null) {
1413 return null;
1414 }
1415 // Action to Preferences
1416 const openUpdatesAction = new models.ExternalUrlAction(installUpdateUrl);
1417 // Action to open preferences is configured as `link`
1418 metricsHelpersClicks.addClickEventToAction(objectGraph, openUpdatesAction, {
1419 targetType: "link",
1420 pageInformation: context.metricsPageInformation,
1421 id: `${context.index}`,
1422 locationTracker: context.metricsLocationTracker,
1423 });
1424 // Shelf model
1425 const upgradeControlText = objectGraph.loc.string("CLIENT_CONTROL_OS_UPGRADE_TITLE", "CHECK FOR UPDATE");
1426 const upgradeControl = new models.ClientControlButton(upgradeControlText, openUpdatesAction);
1427 // Add impressions
1428 addImpressionsFieldsToModel(objectGraph, upgradeControl, context);
1429 const shelf = new models.Shelf("clientControlButton");
1430 shelf.items = [upgradeControl];
1431 return shelf;
1432}
1433/**
1434 * Ingests EI canvas modules of form:
1435 * {
1436 * id: <editorial-id>,
1437 * type: "editorial-item-shelves",
1438 * }
1439 * with additional data:
1440 * - Upsell data on `context.additionalData`
1441 * - Icon Artwork data (iOS only) on `context.additionalData`
1442 *
1443 * to generate an shelf that promotes Arcade service.
1444 *
1445 * @param arcadeShowcaseModule Arcade showcase module
1446 * @param articleData The data backing the article containing the module. Used for top-level relationship.
1447 * @param context Parse context for this page parsing. This context contains the additional requirements data.
1448 */
1449function createArcadeShowcase(objectGraph, arcadeShowcaseModule, articleData, context) {
1450 const supportedOnPlatform = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision;
1451 if (!supportedOnPlatform) {
1452 return null;
1453 }
1454 // Default to upsell on relation, falling back to upsell that may have been fetched separately for orphaned acquisition modules.
1455 let upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData);
1456 if (!upsellData && context.additionalData) {
1457 const upsellResponse = context.additionalData.get("upsellForNonacquisitionCanvas");
1458 upsellData = arcadeCommon.upsellFromContentsOfUpsellResponse(objectGraph, upsellResponse);
1459 }
1460 if (!serverData.isDefinedNonNull(upsellData)) {
1461 return null;
1462 }
1463 const baseMetricsOptions = {
1464 pageInformation: context.metricsPageInformation,
1465 locationTracker: context.metricsLocationTracker,
1466 };
1467 // Flow to See All games if subscribed
1468 const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision);
1469 if (preprocessor.GAMES_TARGET) {
1470 subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
1471 }
1472 else {
1473 subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
1474 }
1475 // Flow to Arcade Subscribe page if unsubscribed.
1476 let unsubscribedAction;
1477 const unsubscribedActionTitle = breakoutsCommon.callToActionLabelFromData(objectGraph, upsellData.marketingItemData);
1478 if (serverData.isDefinedNonNullNonEmpty(unsubscribedActionTitle)) {
1479 // We support an inline offer here instead, when the pricing token is there.
1480 unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, models.marketingItemContextFromString("editorialItemCanvas"), baseMetricsOptions);
1481 if (serverData.isDefinedNonNull(unsubscribedAction)) {
1482 unsubscribedAction.title = unsubscribedActionTitle;
1483 }
1484 }
1485 else {
1486 // If Upsell EI is misconfigured and missing `breakoutCallToActionLabel`, default to opening Arcade app for unsubscribed state.
1487 unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision);
1488 if (preprocessor.GAMES_TARGET) {
1489 unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
1490 }
1491 else {
1492 unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
1493 }
1494 }
1495 const arcadeShowcase = new models.ArcadeShowcase(unsubscribedAction, subscribedAction);
1496 const unsubscribedDescription = arcadeUpsell.descriptionFromData(objectGraph, upsellData.marketingItemData);
1497 arcadeShowcase.unsubscribedDescription = unsubscribedDescription;
1498 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);
1499 if (preprocessor.GAMES_TARGET) {
1500 offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore");
1501 }
1502 else {
1503 offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE");
1504 }
1505 arcadeShowcase.offerDisplayProperties = offerDisplayProperties;
1506 const showcaseMetricsOptions = {
1507 ...baseMetricsOptions,
1508 targetType: "arcadeShowcase",
1509 title: unsubscribedActionTitle,
1510 id: arcadeShowcaseModule.id,
1511 kind: "arcadeShowcase",
1512 softwareType: null,
1513 displaysArcadeUpsell: true,
1514 };
1515 metricsHelpersImpressions.addImpressionFields(objectGraph, arcadeShowcase, showcaseMetricsOptions);
1516 // Build Artwork for iOS only
1517 if (objectGraph.host.isiOS || objectGraph.client.isVision) {
1518 // Context should have additional data to source icons.
1519 if (serverData.isNull(context.additionalData)) {
1520 return null;
1521 }
1522 const iconResponse = context.additionalData.get("arcadeIcons");
1523 if (serverData.isDefinedNonNullNonEmpty(iconResponse)) {
1524 const iconMetricsOptions = {
1525 pageInformation: context.metricsPageInformation,
1526 locationTracker: context.metricsLocationTracker,
1527 };
1528 const iconsDataCollection = mediaDataStructure.dataCollectionFromResultsListContainer(iconResponse);
1529 arcadeShowcase.iconArtworks = content.impressionableAppIconsFromDataCollection(objectGraph, iconsDataCollection, iconMetricsOptions, {
1530 useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */,
1531 });
1532 }
1533 }
1534 const shelf = new models.Shelf("arcadeShowcase");
1535 shelf.items = [arcadeShowcase];
1536 const shelfHasBackgroundColor = objectGraph.host.isiOS || objectGraph.client.isVision;
1537 if (shelfHasBackgroundColor) {
1538 shelf.background = {
1539 type: "color",
1540 color: arcadeShowcaseShelfBackgroundColor,
1541 };
1542 }
1543 return shelf;
1544}
1545// endregion
1546function contentFromModule(objectGraph, module, context) {
1547 const contents = mediaRelationships.relationshipData(objectGraph, module, "contents");
1548 if (!contents) {
1549 return null;
1550 }
1551 return contents;
1552}
1553function addImpressionsFieldsToModel(objectGraph, model, context, impressionData) {
1554 if (!model) {
1555 return;
1556 }
1557 let impressionType = context.module;
1558 if (context.subStyle) {
1559 impressionType = impressionType + "_" + context.subStyle;
1560 }
1561 if (serverData.isNull(impressionData)) {
1562 impressionData = {
1563 id: `${context.index}`,
1564 impressionIndex: context.index,
1565 idType: "sequential",
1566 impressionType: impressionType,
1567 kind: "iosModule",
1568 };
1569 }
1570 model.impressionMetrics = new models.ImpressionMetrics(metricsHelpersUtil.sanitizedMetricsDictionary(impressionData));
1571}
1572//# sourceMappingURL=article.js.map