import * as validation from "@jet/environment/json/validation"; import * as models from "../../api/models"; import * as serverData from "../../foundation/json-parsing/server-data"; import { attributeAsBooleanOrFalse } from "../../foundation/media/attributes"; import { openTVArcadeAppAction } from "../arcade/arcade-common"; import { editorialNotesFromData } from "../content/content"; import * as metricsHelpersLocation from "../metrics/helpers/location"; import { cardDisplayStyleFromData, collapsedHeadingForTodayCard, defaultTodayCardConfiguration, todayCardFromData, } from "./today-card-util"; import { TodayCardDisplayStyle, TodayCardMetricsDisplayStyle, TodayParseContext, } from "./today-types"; const supportedSmallHorizontalCardKinds = new Set([ "artwork", "appIcon", "grid", "multiApp", "video", ]); const supportedMediumHorizontalCardKinds = new Set([ "brandedSingleApp", "artwork", "appIcon", "grid", "multiApp", "video", ]); const supportedLargeHorizontalCardKinds = new Set([ "brandedSingleApp", "artwork", "appIcon", "grid", "multiApp", "video", ]); const iOSSupportedSmallHorizontalCardKinds = new Set([ "brandedSingleApp", "artwork", "grid", "video", ]); const webSupportedSmallHorizontalCardKinds = new Set([ "brandedSingleApp", "artwork", "river", "appIcon", "video", ]); // MARK: - Horizontal Cards /** * The crop code to use for a card using the provided horizontal card shelf content type. * @param {ShelfContentType} contentType The content type of the shelf's items. * @returns {CropCode} The crop code to use for the artwork. */ function horizontalCardCropCodeForContentType(objectGraph, contentType, cardDisplayStyle) { switch (contentType) { case "smallStoryCard": if (objectGraph.host.isTV) { return cardDisplayStyle === TodayCardDisplayStyle.Video ? null : { defaultCrop: "fo" }; } else if (objectGraph.host.isiOS) { return null; } else if (objectGraph.client.isWeb) { return { defaultCrop: "sr" }; } else { return { defaultCrop: "em" }; } case "mediumStoryCard": return { defaultCrop: "el" }; case "largeStoryCard": return cardDisplayStyle === TodayCardDisplayStyle.Video ? { defaultCrop: "sr" } : { defaultCrop: "ek" }; default: return null; } } export function isHorizontalCardSupportedForKind(objectGraph, kind, contentType) { switch (contentType) { case "smallStoryCard": if (objectGraph.host.isiOS) { return iOSSupportedSmallHorizontalCardKinds.has(kind); } else if (objectGraph.client.isWeb) { return webSupportedSmallHorizontalCardKinds.has(kind); } else { return supportedSmallHorizontalCardKinds.has(kind); } case "mediumStoryCard": return supportedMediumHorizontalCardKinds.has(kind); case "largeStoryCard": return supportedLargeHorizontalCardKinds.has(kind); case "todayCard": return objectGraph.client.isWatch; default: return false; } } export function horizontalCardItemsFromCards(objectGraph, cards, contentType, context, contentUnavailable, horizontalShelfCardConfig) { const items = []; for (const cardData of cards) { const todayCard = horizontalCardItemFromCard(objectGraph, cardData, contentType, context, contentUnavailable, horizontalShelfCardConfig); if (serverData.isNullOrEmpty(todayCard)) { continue; } items.push(todayCard); metricsHelpersLocation.nextPosition(context.metricsLocationTracker); } return items; } export function horizontalCardItemFromCard(objectGraph, cardData, contentType, context, contentUnavailable, horizontalShelfCardConfig) { const parseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker); if (horizontalShelfCardConfig === null || horizontalShelfCardConfig === undefined) { horizontalShelfCardConfig = defaultTodayCardConfiguration(objectGraph); horizontalShelfCardConfig.isHorizontalShelfContext = true; } if (objectGraph.client.deviceType !== "watch") { // All today cards are deserialized through this code path on watchOS. // We support all substyles, so we need to not specify this override. horizontalShelfCardConfig.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; } const cardDisplayStyle = cardDisplayStyleFromData(cardData, horizontalShelfCardConfig.coercedCollectionTodayCardDisplayStyle); horizontalShelfCardConfig.prevailingCropCodes = horizontalCardCropCodeForContentType(objectGraph, contentType, cardDisplayStyle); horizontalShelfCardConfig.horizontalCardContentType = contentType; if (!serverData.isDefinedNonNull(cardData.attributes)) { if (contentUnavailable) { contentUnavailable(cardData); } return null; } const todayCard = todayCardFromData(objectGraph, cardData, horizontalShelfCardConfig, parseContext, context.augmentingData); // Ignore the Date here, as we don't care about using it as a refresh signal when embedded in a page other than Today for now. if (serverData.isNull(todayCard)) { return null; } if (objectGraph.client.isiOS) { todayCard.collapsedHeading = collapsedHeadingForTodayCard(objectGraph, todayCard); todayCard.inlineDescription = featuredInDescriptionForCard(objectGraph, todayCard, cardData); // In certain fallback cases, todayCardFromData() will make title and heading the same. // If our generated heading and description are the same, we just show the heading. if (todayCard.inlineDescription === todayCard.collapsedHeading) { todayCard.inlineDescription = null; } // We want to avoid displaying the branded title over AotD/GotD cards, but only when they are not using // the fallback art. const brandedMedia = todayCard.media; if (brandedMedia && brandedMedia.kind === "brandedSingleApp") { // LOC: GLOBAL: App Store on iOS: Word wrapping in App/Game of the... // Branded text on inline today cards is kind of a mess visually, so rely on the inline // description. todayCard.title = null; } } if (contentType === "largeStoryCard") { const heroMedia = todayCard.heroMedia; if (!serverData.isDefinedNonNull(heroMedia) || (heroMedia.artworks.length === 0 && heroMedia.videos.length === 0)) { return null; } } if (isHorizontalCardSupportedForKind(objectGraph, todayCard.media.kind, contentType)) { if (serverData.isDefinedNonNull(context.filterOutMediaCardKinds) && context.filterOutMediaCardKinds.has(todayCard.media.kind)) { return null; } } // Override `todayCard.clickAction` for some special cards. overrideClickActionForTVAcquisitionCardIfNeeded(objectGraph, todayCard, cardData); return todayCard; } export function shelfForHorizontalCardItems(objectGraph, cards, contentType, title, subtitle, context, contentUnavailable) { if (!serverData.isDefinedNonNull(contentType)) { return null; } const shelf = new models.Shelf(contentType); if (title) { shelf.title = title; } shelf.subtitle = subtitle; shelf.isHorizontal = true; switch (contentType) { case "todayBrick": // As iOS doesn't support horizontalCardItemsFromCards, we map the small/medium/large story cards to todayBricks instead. shelf.items = [ featuredInTodayCardsFromData(objectGraph, cards, context.metricsPageInformation, context.metricsLocationTracker, () => true, contentUnavailable), ]; break; default: shelf.items = horizontalCardItemsFromCards(objectGraph, cards, contentType, context, contentUnavailable); break; } return shelf; } /** * Creates a shelf for mini today cards. * * @param objectGraph - The application store object graph. * @param cards - An array of media data structures. * @param contentType - The type of content for the shelf. * @param title - The title of the shelf. * @param subtitle - The subtitle of the shelf. * @param context - The context for parsing today data. * @param contentUnavailable - Optional handler for unavailable content. * @returns A shelf containing today cards. */ export function shelfForMiniTodayCards(objectGraph, cards, title, subtitle, context, contentUnavailable) { const shelf = new models.Shelf("miniTodayCard"); if (title) { shelf.title = title; } shelf.subtitle = subtitle; shelf.isHorizontal = true; const items = []; const cardConfig = defaultTodayCardConfiguration(objectGraph); cardConfig.metricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; cardConfig.alwaysShowBadgeInSmallCards = true; cardConfig.alwaysUseMaterialBlur = true; for (const cardData of cards) { if (!serverData.isDefinedNonNull(cardData.attributes)) { if (contentUnavailable) { contentUnavailable(cardData); } continue; } const todayCard = todayCardFromData(objectGraph, cardData, cardConfig, context); if (serverData.isNullOrEmpty(todayCard)) { continue; } items.push(todayCard); metricsHelpersLocation.nextPosition(context.locationTracker); } shelf.items = items; return shelf; } // MARK: - Featured In /** * Determines the description for the card, were it to appear inline. * @param {TodayCard} card The card in question (for legacy support) * @param {Data} data the card data to get the description from * @returns {string} The description text to use for the card. */ function featuredInDescriptionForCard(objectGraph, card, data) { const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; if (isSmallStoryCardsSupported) { let editorialName = editorialNotesFromData(objectGraph, data, "name"); if (serverData.isDefinedNonNull(editorialName)) { editorialName = editorialName.replace(/\n/g, " "); } switch (card.media.kind) { case "brandedSingleApp": if (card.overlay instanceof models.TodayCardMarketingLockupOverlay && serverData.isDefinedNonNull(card.overlay.lockup)) { return card.overlay.lockup.title; } else { return editorialName; } default: return editorialName; } } switch (card.media.kind) { case "brandedSingleApp": if (card.overlay instanceof models.TodayCardThreeLineOverlay) { return card.overlay.heading; } else if (serverData.isDefinedNonNull(card.title)) { return card.title.replace(/\n/g, " "); } return null; default: const unformattedDescription = objectGraph.loc.string("TODAY_BRICK_STANDARD_DESCRIPTION"); let heading; let title; if (card.overlay instanceof models.TodayCardThreeLineOverlay) { heading = card.overlay.heading; title = card.overlay.title; } else { heading = card.heading; title = card.title; } if (!unformattedDescription || !heading || !title) { return null; } return unformattedDescription.replace("{heading}", heading).replace("{title}", title); } } export function featuredInTodayCardsFromData(objectGraph, dataArray, metricsPageInformation, metricsLocationTracker, isIncluded, contentUnavailable) { return validation.context("todayCardsFromPlatformData", () => { if (!dataArray) { return null; } const featuredInCardConfig = defaultTodayCardConfiguration(objectGraph); featuredInCardConfig.enableFullScreenVideo = false; featuredInCardConfig.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; featuredInCardConfig.prevailingCropCodes = { defaultCrop: "fo" }; featuredInCardConfig.isHorizontalShelfContext = true; const context = new TodayParseContext(metricsPageInformation, metricsLocationTracker); const todayCards = []; for (const data of dataArray) { if (!serverData.isDefinedNonNull(data.attributes)) { if (contentUnavailable) { contentUnavailable(data); } continue; } const todayCard = todayCardFromData(objectGraph, data, featuredInCardConfig, context); // Ignore the Date here, as we don't care about using it as a refresh signal when embedded in a page other than Today for now. if (serverData.isNull(todayCard)) { continue; } if (isIncluded(todayCard)) { todayCard.collapsedHeading = collapsedHeadingForTodayCard(objectGraph, todayCard); todayCard.inlineDescription = featuredInDescriptionForCard(objectGraph, todayCard, data); // In certain fallback cases, todayCardFromData() will make title and heading the same. // If our generated heading and description are the same, we just show the heading. const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; if (isSmallStoryCardsSupported && todayCard.inlineDescription === todayCard.collapsedHeading) { todayCard.inlineDescription = null; } // We want to avoid displaying the branded title over AotD/GotD cards, but only when they are not using // the fallback art. const brandedMedia = todayCard.media; if (brandedMedia && brandedMedia.kind === "brandedSingleApp") { // LOC: GLOBAL: App Store on iOS: Word wrapping in App/Game of the... // Branded text on inline today cards is kind of a mess visually, so rely on the inline // description. todayCard.title = null; } todayCards.push(todayCard); } } if (!todayCards.length) { return null; } return new models.InlineTodayCards(todayCards); }); } // MARK: - tvOS Acquisition EI Card /** * On tvOS, some special editorial items are supposed to link directly into the Apple Arcade app. * This is to override the click action if needed. * @param card Today card to potentially modify. * @param data Data that card was built from. */ function overrideClickActionForTVAcquisitionCardIfNeeded(objectGraph, card, data) { if (objectGraph.client.deviceType !== "tv" || !attributeAsBooleanOrFalse(data, "isAcquisition")) { return; // Only override on acquisition stories on tvOS. } const openArcadeAppAction = openTVArcadeAppAction(objectGraph); card.clickAction = openArcadeAppAction; } // endregion //# sourceMappingURL=today-horizontal-card-util.js.map