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 mediaDataFetching from "../../../foundation/media/data-fetching"; import * as mediaNetwork from "../../../foundation/media/network"; import * as mediaRelationship from "../../../foundation/media/relationships"; import { ResponseMetadata } from "../../../foundation/network/network"; import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; import * as urls from "../../../foundation/network/urls"; import * as contentAttributes from "../../content/attributes"; import * as content from "../../content/content"; import * as flowPreview from "../../content/flow-preview"; import * as filtering from "../../filtering"; import * as lockups from "../../lockups/lockups"; import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; import { GroupingShelfController } from "./grouping-shelf-controller"; import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; import { makeGameCenterHeader } from "../../arcade/arcade-common"; import { injectCrossfireFlowForGameCenter } from "./grouping-game-center-popular-with-your-friends-shelf-controller"; export class GroupingGameCenterContinuePlayingShelfController extends GroupingShelfController { // region Constructors constructor() { super("GroupingGameCenterContinuePlayingShelfController"); this.batchGroupKey = "gameCenterContinuePlaying"; this.supportedFeaturedContentIds = new Set([500 /* groupingTypes.FeaturedContentID.AppStore_ContinuePlayingMarker */]); } // endregion // region GroupingShelfController _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { return (super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) && this.supportsVideoCardShelf(objectGraph, objectGraph.host.platform)); } // endregion // region Shelf Builder shelfRoute(objectGraph) { return [ ...super.shelfRoute(objectGraph), { protocol: Protocol.internal, path: `/${Path.grouping}/${Path.shelf}/{token}`, query: [Parameters.isGameCenterContinuePlayingShelf], }, ]; } // endregion // region Shelf Creation Prerequisites /** * For a given mediaApiData extract the actual shelfContents array needed to render this shelf * * @param mediaApiData The outer shelfContents object containing the shelf contents */ initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; } /** * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` * needed to render this shelf * * @param objectGraph The App Store dependency graph * @param shelfUrl The url that this controller handled on a secondary fetch * @param parameters The extracted parameters from the shelf url */ async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { const startTime = Date.now(); const maxNumberOfGames = this.maximumNumberOfRecentGamesToRequest(); const recentlyPlayedGamesPromise = objectGraph.gameCenter.fetchRecentlyPlayedGamesWithinSeconds(this.gameCategoryFilter(shelfToken.gamesFilter), maxNumberOfGames, objectGraph.bag.recentlyPlayedGamesWindowInSeconds); return await recentlyPlayedGamesPromise.then(async (recentlyPlayedIds) => { const endTime = Date.now(); objectGraph.console.log("grouping-gamecenter-builder: requestForContinuePlaying NATIVE took " + (endTime - startTime).toString(10) + " milliseconds."); let shelfDataPromise; if (recentlyPlayedIds.length === 0) { // One of the few exceptions where TS minifiers cannot derive data container type // without some help, in this case using explicit type for the value passed to promise. const emptyShelfData = { shelfContents: [] }; return await Promise.resolve(emptyShelfData); } else { const request = new mediaDataFetching.Request(objectGraph) .withIdsOfType(recentlyPlayedIds.slice(0, this.maximumNumberOfRecentGamesToShow()), "apps") .includingAgeRestrictions(); groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, request); shelfDataPromise = mediaNetwork .fetchData(objectGraph, request, {}) .then((dataContainer) => { const shelfData = { shelfContents: dataContainer.data, responseTimingValues: dataContainer[ResponseMetadata.timingValues], }; return shelfData; }); } return await shelfDataPromise; }); } /** * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type * * @param objectGraph The App Store dependency graph * @param baseShelfToken The base grouping shelf token created by the grouping-controller * @param mediaApiData The outer data object containing the FC properties and data * @param groupingParseContext The parse context for the grouping page so far */ shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { return baseShelfToken; } incompleteShelfFetchStrategy(objectGraph) { return models.IncompleteShelfFetchStrategy.OnPageLoad; } // endregion // region Shelf Creation /** * * @param objectGraph The App Store dependency graph * @param shelfToken The shelf shelfToken for this current shelf creation request * @param shelfData The media api shelfContents array for this shelf * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, * this will be missing when this controller renders a secondary or incomplete shelf fetch. */ _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { if (shelfToken.isFirstRender) { return this.pendingContinuePlayingForGrouping(objectGraph, shelfToken); } else { return this.continuePlayingShelfForGrouping(objectGraph, shelfData.shelfContents, shelfToken); } } pendingContinuePlayingForGrouping(objectGraph, shelfToken) { const shelf = this.continuePlayingShelfForGrouping(objectGraph, [], shelfToken); if (!shelf) { return null; } const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); shelf.url = groupingShelfUrl.param(Parameters.isGameCenterContinuePlayingShelf, "true").build(); shelf.batchGroup = this.batchGroupKey; return shelf; } continuePlayingShelfForGrouping(objectGraph, shelfContents, shelfToken) { return validation.context("continuePlayingShelfForGrouping", () => { const shelf = this.videoCardContinuePlayingShelf(objectGraph, shelfContents, shelfToken); shelf.mergeWhenFetched = false; // Always replace shelf.batchGroup = this.batchGroupKey; shelf.isHidden = shelf.items.length === 0; shelf.header = makeGameCenterHeader(objectGraph, objectGraph.loc.string("GameCenter.ContinuePlayingShelf.Title"), shelfToken.subtitle); return shelf; }); } // endregion // region Video Card Shelf supportsVideoCardShelf(objectGraph, platform) { switch (platform) { case "iOS": case "tvOS": case "macOS": return true; default: return false; } } videoCardContinuePlayingShelf(objectGraph, dataArray, shelfToken) { return validation.context("videoCardContinuePlayingShelf", () => { const shelf = new models.Shelf("videoCard"); shelf.isHorizontal = true; shelf.batchGroup = this.batchGroupKey; const items = []; for (const data of dataArray) { // Filter out unwanted content if (filtering.shouldFilter(objectGraph, data)) { continue; } const item = this.editorialSplashVideoCardForContinuePlaying(objectGraph, data, shelfToken); if (item) { items.push(item); } } shelf.items = items; return shelf; }); } /** * Create a `VideoCard` configured for CP Shelf. * Specifically, it uses: * - TV Top Shelf Static Image Still * - Arcade Product Page Uber Video * * @param objectGraph * @param data * @param shelfToken */ editorialSplashVideoCardForContinuePlaying(objectGraph, data, shelfToken) { return validation.context("editorialSplashVideoCardForContinuePlaying", () => { var _a; const lockupMetricsOptions = { pageInformation: shelfToken.metricsPageInformation, locationTracker: shelfToken.metricsLocationTracker, targetType: "lockup", }; const shouldHideArcadeHeader = objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && serverData.asBooleanOrFalse(shelfToken.isArcadePage); const isArcadeLockup = content.isArcadeSupported(objectGraph, data); const lockupOptions = { metricsOptions: lockupMetricsOptions, artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, offerEnvironment: "dark", offerStyle: "white", canDisplayArcadeOfferButton: true, shouldHideArcadeHeader: shouldHideArcadeHeader, isSubtitleHidden: isArcadeLockup && !shouldHideArcadeHeader, }; const video = this.editorialSplashVideoWithTopShelfStill(objectGraph, data); if (!video || !video.preview) { return null; } const lockup = lockups.lockupFromData(objectGraph, data, lockupOptions); if (!lockup) { return null; } lockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, lockup.clickAction); const clickAction = this.clickActionForVideoCard(objectGraph, data, objectGraph.host.platform, lockupMetricsOptions, shelfToken.clientIdentifierOverride); if (!clickAction) { return null; } const videoCard = new models.VideoCard(); videoCard.video = video; videoCard.lockup = lockup; videoCard.overlayStyle = "dark"; videoCard.clickAction = clickAction; // Set flow preview actions const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, lockupMetricsOptions); videoCard.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, true, shelfToken.clientIdentifierOverride, videoCard.clickAction, lockupMetricsOptions, metricsClickOptions); // Configure impressions borrowing lockup values for now. const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, lockup.title, lockupMetricsOptions); metricsHelpersImpressions.addImpressionFields(objectGraph, videoCard, impressionOptions); (_a = videoCard.impressionMetrics) === null || _a === void 0 ? true : delete _a.fields["impressionIndex"]; return videoCard; }); } /** * Return a video to use for continue playing with: * - Product Uber * - TV Top Shelf Static Still */ editorialSplashVideoWithTopShelfStill(objectGraph, data) { return validation.context("editorialSplashVideoWithTopShelfStill", () => { // TV Top Shelf Still (If Any): let previewOverride = null; const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.topShelf"); if (serverData.isDefinedNonNull(artworkData)) { previewOverride = content.artworkFromApiArtwork(objectGraph, artworkData, { withJoeColorPlaceholder: true, useCase: 23 /* content.ArtworkUseCase.VideoCardStill */, cropCode: "sr", }); } return content.editorialSplashVideoFromData(objectGraph, data, previewOverride); }); } // endregion // region Helpers /** * Returns the click action to use for given platform for video card */ clickActionForVideoCard(objectGraph, data, platform, metricsOptions, clientIdentifierOverride) { const lockupClickMetricsOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, metricsOptions); let productPageAction = lockups.actionFromData(objectGraph, data, lockupClickMetricsOptions, clientIdentifierOverride); productPageAction = injectCrossfireFlowForGameCenter(objectGraph, productPageAction); // Wrap in Open for tv only. if (platform === "tvOS") { const openAppAction = new models.OpenAppAction(data.id, "app"); const openAppClickOptions = { actionType: "open", id: data.id, contextualAdamId: data.id, anonymizationOptions: metricsOptions.anonymizationOptions, pageInformation: metricsOptions.pageInformation, locationTracker: metricsOptions.locationTracker, }; metricsHelpersClicks.addClickEventToAction(objectGraph, openAppAction, openAppClickOptions); const stateAction = new models.OfferStateAction(data.id, productPageAction); stateAction.openAction = openAppAction; // If the app is downloading and the user clicks this item, take them to the product page instead of cancelling the download. stateAction.cancelAction = productPageAction; return stateAction; } else { return productPageAction; } } /** * The maximum number of games we should ask GameCenterServer for. Note that some of the games it fetches will * not be Arcade nor platform-compatible games. For this reason, you should ask for more than you need. * As this call is performant enough with a time limit, we just request the maximum amount. * * The maximum that GameCenterServer will accept is 200. */ maximumNumberOfRecentGamesToRequest() { return 200; } /** * The maximum number of games to display on the shelf. * * The maximum that Media API will accept is 100. */ maximumNumberOfRecentGamesToShow() { return 10; } /** * Convert GameCategoryFilter to GamesFilter. Ideally these would be the same, but seed 1 has already left the station. * @param gamesFilter */ gameCategoryFilter(gamesFilter) { if (gamesFilter === "nonArcade") { return "nonarcade"; } return gamesFilter; } // endregion // region Metrics shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { return { ...baseMetricsOptions, badges: { gameCenter: true, }, idType: "its_contentId", title: objectGraph.loc.string("GameCenter.ContinuePlayingShelf.Title"), }; } } //# sourceMappingURL=grouping-game-center-continue-playing-shelf-controller.js.map