# Storyteller Kotlin SDK - AI Context URL: ## Task Provide LLM-ready guidance for Storyteller Android SDK integrators. ## Metadata - Slug: ai-bundle - Source: public-docs/*.md bundle - Audience: Android SDK integrators - Platforms: Android - Related: GettingStarted.md, Users.md, PrivacyAndTracking.md, StorytellerLists.md, StorytellerListViews.md, StorytellerDelegates.md, Analytics.md, Ads.md ## Overview - This bundle aggregates the public Storyteller Android docs into one searchable file for cross-cutting integration questions. - Prefer a specific `llms-.txt` file when you know the topic; use this file when setup, navigation, analytics, privacy, ads, themes, and upgrades overlap. - Treat the per-page AI files as the fast path and the canonical excerpts here as the reference layer for fields, defaults, limitations, and migration notes. - Compose is the preferred integration path; legacy Views remain documented for existing integrations. - Version-sensitive behavior lives in `Changelog.md`, so check concrete versions and dates before recommending upgrade work. - Do not invent API signatures, optional parameters, defaults, or analytics fields when the canonical excerpts are silent. ## When To Use - The question spans multiple docs, such as initialization plus privacy, analytics, or ad setup. - You are debugging an issue without knowing whether it belongs to lists, delegates, deep links, themes, or upgrade changes. - You need one attachment for an AI tool but still want access to the raw docs text. ## Integration Steps 1. Identify the most relevant topic from the per-page file list below and read that summary first when possible. 2. Use the canonical excerpts in this bundle to confirm signatures, field names, defaults, supported values, and "only if/when" behavior. 3. Cross-check migrations or recent behavior changes against the `Changelog.md` section before proposing an upgrade fix. 4. If the docs do not state a detail, call that out instead of inferring a new API or default. ## Examples - A consent-related ads question may require `PrivacyAndTracking.md`, `Ads.md`, and `Analytics.md` together. - A player launch bug may span `GettingStarted.md`, `OpenPlayer.md`, `DeepLinking.md`, and `NavigatingToApp.md`. - A row rendering issue may require `StorytellerLists.md` or `StorytellerListViews.md`, plus `Themes.md` and `StorytellerDelegates.md`. ## Pitfalls / Notes - This bundle is large; trim to the relevant sections when your AI tool has a tight context window. - Canonical excerpts intentionally preserve source wording except for removed TOC and image-only lines, so use them for exact field lists and migration notes. - Some sections may change across releases; mention the exact version/date instead of saying "latest" without context. ## Cross-References - Included per-page AI files: ## Canonical Reference # Storyteller Kotlin SDK - AI Context This file aggregates the public Storyteller Kotlin SDK documentation into a single plain-text file for use with AI coding assistants (ChatGPT, Claude, etc.). ## How to Use This File - Provide this file to your AI assistant along with your app code when working with the Storyteller Kotlin SDK. - Internal Markdown links like `[Working with Users](Users.md)` refer to sections labelled `## Source: Users.md` later in this file. - Image references are removed; headings and code examples are preserved. ## Integration Rules for AI 1. Respect the client's existing architecture and Gradle setup; adapt examples accordingly. 2. Prefer Storyteller public APIs and cross-check important behaviors against the official docs at https://docs.getstoryteller.com/android. 3. Do not add optional parameters, fields, or configuration flags unless clearly required. 4. Avoid inventing APIs; use only functions/types present in the SDK or documented here. ## Included Source Sections - index.md - GettingStarted.md - StorytellerModule.md - Users.md - PrivacyAndTracking.md - StorytellerLists.md - StorytellerListViews.md - StorytellerClipsFragment.md - StorytellerHome.md - Cards.md - OpenPlayer.md - Search.md - DeepLinking.md - NavigatingToApp.md - Themes.md - StorytellerDelegates.md - AdditionalMethods.md - Analytics.md - Ads.md - StorytellerBrightcove.md - OpenSourceLicenses.md - Changelog.md - javadoc.md - AI.md --- ## Source: index.md # Storyteller Kotlin SDK Documentation Welcome to the Storyteller Kotlin SDK documentation. This SDK enables you to integrate Storyteller stories, clips, and UI components into your Android app. ## Quick Start New to Storyteller on Android? Start here: - [Quickstart Guide](GettingStarted.md) - [Storyteller Module](StorytellerModule.md) (optional modules and configuration) - [Working with Users](Users.md) (user IDs and custom attributes) - [Privacy and Tracking](PrivacyAndTracking.md) (tracking/personalization controls) ## Documentation ### UI Components - [Jetpack Compose Lists](StorytellerLists.md) - [Legacy XML List Views](StorytellerListViews.md) - [Embedded Clips](StorytellerClipsFragment.md) - [Storyteller Home](StorytellerHome.md) - [Cards](Cards.md) ### Navigation and Discovery - [Open Player](OpenPlayer.md) - [Search](Search.md) - [Deep Linking](DeepLinking.md) - [Navigating to App](NavigatingToApp.md) ### Customization - [Custom Themes](Themes.md) - [Storyteller Delegates](StorytellerDelegates.md) - [Additional Methods](AdditionalMethods.md) ### Integrations - [Analytics](Analytics.md) - [Ads](Ads.md) - [Storyteller Brightcove Module](StorytellerBrightcove.md) ### Reference - [What's New](Changelog.md) - [Open Source Licenses](OpenSourceLicenses.md) - [API Reference (Dokka)](javadoc.md) - [Using Docs with AI](AI.md) ## Support Need help? Contact us at . --- ## Source: GettingStarted.md # Quickstart Guide This is a developers' guide for setting up Storyteller for native Android apps. This guide will cover the basic technical steps for initializing the Storyteller SDK, authenticating a user, and adding a `StorytellerStoriesRowView` to your app. ## Resources - [Storyteller Showcase App](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/README.md#L1) ### Showcase examples - [Compose — SDK init in `ShowcaseApp`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/main/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L40) - [Compose — `Storyteller.initialize` in `StorytellerServiceImpl`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L40) - [XML — SDK init in `ShowcaseApp`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L40) - [XML — `Storyteller.initialize` in `StorytellerServiceImpl`](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L43) ## How to Add the SDK to Your Project Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`. ### SDK Dependencies Currently the Android SDK contains the following [dependencies](https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk/Storyteller/sdk/11.2.2/sdk-11.2.3.pom). ### R8 / ProGuard If your app uses R8, the rules are added automatically. ### SDK Installation The Android SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/). 1. Add the maven repository for the SDK in the _Project_ `build.gradle` file (`MyAwesomeApp/build.gradle`), under the `allprojects` section > Note: make sure it is added to `allprojects`, and not `buildscript` ```groovy ... allprojects { repositories { google() mavenCentral() maven { url 'https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk' } } } ``` 1. Modify the app _Module_ `build.gradle` file (`MyAwesomeApp/app/build.gradle`) ```groovy ... dependencies { def storyteller_version = "11.2.3" implementation(group: "Storyteller", name: "sdk", version: "$storyteller_version") } ``` 1. Sync your project with Gradle files - Android Studio ## SDK Initialization Before using the Android SDK in your app, you need to initialize it with an API key. ### Adding an API Key Use the `initialize(apiKey: String, userInput: StorytellerUserInput? = null, eventTrackingOptions: StorytellerEventTrackingOptions = StorytellerEventTrackingOptions(), onSuccess: () -> Unit = {}, onFailure: (StorytellerError) -> Unit = {})` public method to manually initialize the SDK at runtime. This will authenticate the SDK on the Storyteller API and configure it with the corresponding settings. - `apiKey`: (**Required**) the API key you wish to initialize the SDK with - `userInput` : details of the user to be authenticated (this should be a unique user ID. If this is not set, the default value is used) - `eventTrackingOptions`: privacy and tracking configuration options (defaults to all tracking enabled) - `onSuccess`: callback for successful completion - `onFailure`: callback for failed completion with error Usage: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true // ... other options ), onSuccess = { // onSuccess action }, onFailure = { error -> // onFailure action } ) ``` Initialization errors: - `InitializationError`: when the context is `null` - `InvalidAPIKeyError`: when an invalid API key is used - `NetworkError`: when the call to load the settings for the SDK fails (i.e. a non-success HTTP status code is returned) - `NetworkTimeoutError`: when the call to load the settings for the SDK times out - `JSONParseError`: when a malformed settings response is received from the server > Note: Please be aware that this method is asynchronous ## Authenticate a User For more information about Users and External IDs, please see [Working with Users](Users.md) ## Adding a StorytellerStoriesRow Composable 1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc. ```kotlin @Composable fun MainScreen() { Box() { // ... } } ``` 1. Storyteller Composables Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK. ```kotlin StorytellerStoriesRow( modifier = Modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below. delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below. state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below. ) ``` ## XML Views (Legacy) Preferred way to add Storyteller Lists is to use Composables. Storyteller still supports XML/Views and the guide can be found [in the StorytellerListViews documentation](StorytellerListViews.md). --- ## Source: StorytellerModule.md # StorytellerModule The StorytellerModule module is an interface you can adopt to handle fetching ads and recording user activity events from Storyteller. It contains the following properties and methods: ## Showcase examples - [Compose — initializing modules (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L61) - [XML — initializing modules (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L62) ## Methods ```kotlin val googleAdSource: StorytellerGoogleAdSource? abstract fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) abstract fun getAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onAdFallbackRequest: (StorytellerAdLoadError) -> Unit, onError: (StorytellerAdLoadError) -> Unit ) fun getBottomBannerAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (StorytellerAdLoadError) -> Unit ) ``` ### Google Ad Source The optional `googleAdSource` property declares whether your module integrates with Google ad inventory. This value is used for ad analytics source fields and Google module normalization. - Return `StorytellerGoogleAdSource.GAM` for Google Ad Manager integrations. - Return `StorytellerGoogleAdSource.ADMOB` for Google AdMob integrations. - Return `null` for non-Google custom ad providers. `StorytellerGamModule` and `StorytellerAdMobModule` set this value automatically. ### Analytics The callback `onUserActivityOccurred` provides analytics events and corresponding data to be triggered internally by the SDK. This information can be used in your app. The following parameters are passed to the callback method: - `type` - type of event that occurred, as a `StorytellerUserActivity.EventType` enum - `data` - an object containing data about the event which occurred Example: ```kotlin override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) { when (type) { StorytellerUserActivity.EventType.AD_ACTION_BUTTON_TAPPED -> onAdAction(data) StorytellerUserActivity.EventType.OPENED_AD -> onAdStart(data) StorytellerUserActivity.EventType.FINISHED_AD -> onAdEnd(data) else -> Log.d("StorytellerModule", "Unhandled event type: $type") } } ``` See the [Analytics](Analytics.md) page for more information. ### Ads By implementing `getAd` and `getBottomBannerAd`, you can provide custom ad data for the SDK to render. This is only applicable when the ad configuration is set to `Integrating App` in the CMS. Ad data can be obtained asynchronously, and should be returned via the appropriate callback. #### getAd The `getAd` method is called when the SDK needs a fullscreen ad. The `onComplete` callback should be called when the ad data is ready, and the `onError` callback should be called for terminal failures. If your implementation falls back to another ad request after a failure, invoke `onAdFallbackRequest` with the error from the failed attempt instead of calling `onError`. This lets the SDK record the failure and emit a new ad-request event. Only call `onError` when no more fallback attempts will be made. ```kotlin override fun getAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onAdFallbackRequest: (StorytellerAdLoadError) -> Unit, onError: (StorytellerAdLoadError) -> Unit, ) { val ad = fetchAd(adContext, adRequestInfo) if (ad != null) { onComplete(ad) } else { onError(StorytellerAdLoadError(adErrorMessage = "Failed to fetch ad")) } } ``` #### getBottomBannerAd The `getBottomBannerAd` method is called when the SDK needs a bottom banner ad (displayed at the bottom of clips). The `adContext.isBottomBanner` property will be `true` for these requests. Only 300x50 and 320x50 ad sizes are supported. ```kotlin override fun getBottomBannerAd( adContext: AdContext, adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit, onError: (StorytellerAdLoadError) -> Unit ) { val bannerAd = fetchBottomBannerAd(adContext, adRequestInfo) if (bannerAd != null) { onComplete(bannerAd) } else { onError(StorytellerAdLoadError(adErrorMessage = "Failed to fetch bottom banner ad")) } } ``` For a detailed discussion of all the relevant considerations, please see the dedicated [Ads](Ads.md) page. ## Technical Consideration Use of a custom StorytellerModule is optional. If you do not implement this interface, the SDK will use the default implementation see [Ads](Ads.md). It must be provided to the SDK when initializing the Storyteller SDK using the `Storyteller.modules` method to see GAM or AdMob Ads. **Note**: Only one ad module should be used at a time. If using `StorytellerGamModule` or `StorytellerAdMobModule`, do not register both simultaneously. See the [Ads](Ads.md) page for details on choosing between modules. --- ## Source: Users.md # Working with Users User IDs can be used for reporting purposes, storing the read status of Clips and Stories, followed categories and user preferences. By default, the Storyteller SDK creates an `externalId` for users when the SDK is first initialized. Each time `null` value is passed to `Storyteller.initialize(userInput = null)` SDK will handle user management and the default `externalId` is stored until the user uninstalls the app. **However, if you have a user account system then you may wish to set your own user IDs within the Storyteller SDK. The `externalId` should be an identifier that is unique per user and does not change. Therefore, using something like the user's email address is not a good choice for an `externalId` as the user may change it at some point in the future. However, using a unique UUID/GUID would be a good choice as it is guaranteed to not change over time.** > Note: Storyteller SDK is not storing any user IDs passed to initialize. We follow VPPA compliance and the user IDs are hashed during the App runtime ## Showcase examples - [Compose — user ID, custom attributes, locale (`AccountScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/account/AccountScreen.kt#L113) - [Compose — initialization + user attribute updates (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L114) - [XML — login + verify flow (user ID)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/login/LoginViewModel.kt#L53) - [XML — initialization + user attribute updates (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L94) ## Setting a User ID In order to supply a `externalId` to the Storyteller SDK, call the following method: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-externalId"), onSuccess = { // onSuccess action }, onFailure = { error -> // onFailure action } ) ``` This should be called as soon as the value for the `externalId` is known in your app. For example, this could be on app start or when a user logs in. ## Current User Information For VPPA compliance, the SDK does not expose the current user ID. If you need it for your app logic, keep it in your own user/session store. You can access the following SDK information via these properties: ```kotlin Storyteller.currentApiKey // Returns the current API key (avoid logging) Storyteller.isInitialized // Returns whether the SDK is initialized Storyteller.version // Returns the SDK version ``` ## Changing Users If you use login in your app and wish to allow users to logout and log back in as a new user (or proceed as an anonymous user) then when a user logs out you should call `initialize` again, specifying a new `externalId`. Note that this will reset the local store of which Pages the user has viewed. ## Setting the User's Locale In order to set the locale to the Storyteller SDK, call the following method: ```kotlin Storyteller.user.setLocale(locale: String?) ``` Ensure the locale parameter uses an ISO 639-1 two-letter if available, otherwise, use an ISO 639-2 three-letter language code for precise language specification: ```kotlin Storyteller.user.setLocale("fr") Storyteller.user.setLocale("ace") ``` To clear or reset the locale settings, simply pass `null` as the parameter: ```kotlin Storyteller.user.setLocale(null) ``` ## User Customization ### Custom Attributes One of the features of Storyteller is the ability to set custom attributes for requests. These custom attributes can be used to provide additional information about a request, such as the user's location or the device they are using. It can be also used for audience targeting. ### How to Use To set a custom attribute, you can use the `setCustomAttribute()` method of the `Storyteller.user` object. This method takes two parameters: the key of the custom attribute and its value. For example, to set a custom attribute for the user's location, you can use the following code: ```kotlin Storyteller.user.setCustomAttribute("location", "New York") ``` This will add a custom attribute to all requests made by the user, with the key "location" and the value "New York". You can set multiple custom attributes by calling setCustomAttribute() multiple times with different keys and values. Only String values are allowed. > Setting custom attributes should be done after Storyteller SDK is initialized. To remove custom attribute, you can use the `removeCustomAttribute()` method of the `Storyteller.user` object. This method takes one parameter: the key of the custom attribute to remove. For example, to remove a custom attribute for the user's location, you can use the following code ```kotlin Storyteller.user.removeCustomAttribute("location") ``` ## Followed Categories Storyteller provides a mechanism for managing followed categories associated with clips. ### How to Use #### Set followed categories To set a followed category, call the `addFollowedCategory()` function for a singular category or `addFollowedCategories()` for a collection. Access these methods through the `Storyteller.user` object. For instance, to designate a followed category attribute for the user, implement the following code: ```kotlin Storyteller.user.addFollowedCategory("location") ``` or ```kotlin Storyteller.user.addFollowedCategories(listOf("location", "city", "country")) ``` These categories are stored locally and used to decide whether the plus or tick icon is displayed in the clips UI. > Setting followable categories attributes should be done after Storyteller SDK is initialized. #### Remove followable category To remove a followed category, use the `removeFollowedCategory()` method of the `Storyteller.user` object. This method requires a single parameter—the key of the followed category to be removed. For example, to remove a followable category related to the user's location, utilize the following code: ```kotlin Storyteller.user.removeFollowedCategory("location") ``` This function accepts a singular string parameter, removing the specified category from the locally stored list. ## Storyteller-Managed Following The SDK handles following activity internally and automatically syncs with Storyteller's servers. This simplifies integration by removing the need for custom following logic in your app. This means that all of the above methods on `Storyteller.user` are no-ops in this mode. Also the `categoryFollowActionTaken` is not called. ### Common methods To check if a specific category is being followed, use the `isCategoryFollowed()` method. To retrieve a list of all currently followed categories, use the `getFollowedCategories()` method. ```kotlin val isFollowing: Boolean = Storyteller.user.isCategoryFollowed("location") val followedCategories: Set = Storyteller.user.followedCategories ``` --- ## Source: PrivacyAndTracking.md # Privacy and Tracking The `eventTrackingOptions` parameter customizes Storyteller's analytics and tracking behavior during SDK initialization. It allows certain features to be disabled based on user privacy choices. It is an object of type `StorytellerEventTrackingOptions` and by default, all of its properties are enabled: ```kotlin data class StorytellerEventTrackingOptions( val enablePersonalization: Boolean = true, val enableStorytellerTracking: Boolean = true, val enableUserActivityTracking: Boolean = true, val enableAdTracking: Boolean = true, val enableFullVideoAnalytics: Boolean = true, val enableRemoteViewingStore: Boolean = true ) // Set during initialization Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions(/*your setup here*/) ) // Read current settings (read-only) val currentOptions = Storyteller.eventTrackingOptions ``` > **Note:** Starting from version 11.0.0, `eventTrackingOptions` can only be set during SDK initialization and cannot be modified afterwards. To change these settings, you must call `Storyteller.initialize()` again with new options. ## Showcase examples - [Compose — privacy toggles (`AccountViewModel`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/account/AccountViewModel.kt#L83) - [Compose — stored tracking preferences (`SessionRepositoryImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/SessionRepositoryImpl.kt#L47) - [XML — re-initialize with `eventTrackingOptions` (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L95) ## User Personalization When `enablePersonalization` is enabled, user attributes and the user's ID are included on requests to Storyteller's servers to allow us to personalize the content returned ## Storyteller tracking When `enableStorytellerTracking` is enabled, we will record analytics events on our servers. _Note_ that some events are necessary for user functionality and will still be transmitted (but not stored) even when this setting is off. ## User Activity tracking When `enableUserActivityTracking` is enabled, we will call the Storyteller delegate's method `onUserActivityOccurred()`, which allows integrating apps to record our analytics events on their own systems. ## Ads tracking When `enableAdTracking` is disabled, ad-related events will not be tracked through `onUserActivityOccurred()` Storyteller delegate method and on our servers. Additionally, only necessary fields like Ad Unit Id and Custom Template Id's will be included in GAM requests. ## Full Video Analytics When `enableFullVideoAnalytics` is enabled, detailed video information (story titles, clip titles, story IDs, clip IDs, etc.) is included in analytics events sent to the `onUserActivityOccurred()` delegate method. When disabled, sensitive video data fields are nullified for VPPA (Video Privacy Protection Act) compliance: - `storyId`, `storyTitle`, `storyDisplayTitle` - `clipId`, `clipTitle` - `pageId`, `pageTitle`, `itemTitle`, `containerTitle` - `cardId`, `cardTitle`, `cardSubtitle` This setting allows integrators to comply with video privacy regulations while still receiving engagement analytics events. All other analytics data (user interactions, durations, event types, etc.) continues to be provided. ## Remote Viewing Store When `enableRemoteViewingStore` is enabled (default), the SDK operates normally with full user activity tracking and remote storage capabilities. When `enableRemoteViewingStore` is disabled, the SDK operates in a privacy-enhanced mode designed to address VPPA (Video Privacy Protection Act) compliance concerns: - **No User ID Storage**: User IDs are never stored locally or sent to backend services - **Local-Only User Activity**: All user viewing activity is stored locally on the device and never synchronized with remote servers - **No Remote User Activity Fetch**: The SDK will not attempt to fetch user activity data from remote servers This mode is particularly useful for clients who need enhanced privacy protection while still maintaining local user experience features like viewing history and recommendations. Example for enhanced VPPA compliance: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true, enableUserActivityTracking = true, enableAdTracking = true, enableFullVideoAnalytics = false, // Nullifies sensitive video data enableRemoteViewingStore = false // Disables remote user ID storage and tracking ) ) ``` ## Disabled Functional Features The `disabledFunctionalFeatures` property allows you to selectively disable specific functional areas of the SDK while maintaining others. This provides granular control over which user interactions are tracked and stored. When a functional feature is disabled: - The SDK will not make API requests for that particular item (even if `enableRemoteViewingStore` is true) - The SDK will not store the relevant activity locally on device - The UI will behave as if the feature is disabled from the server Available functional features that can be disabled: - **All** - Disables everything including any future cases - **PageReadStatus** - Disables Page Read/Unread feature - **ClipViewedStatus** - Disables Clips Viewed/Not Viewed feature - **ClipLikes** - Disables Clip Likes and Unlikes - **ClipShares** - Disables Clip Share tracking and storage (but not the act of sharing) ### UI Behavior When Features Are Disabled **Pages**: When PageReadStatus is disabled, all rows act as if Read/Unread tracking is disabled from the server. **Clips**: When ClipViewedStatus is disabled, all entry points act as if Viewed/Not Viewed tracking is disabled from the server. **Clip Likes**: When disabled, users can like/unlike clips with UI feedback, but if they swipe away and return, the clip will appear unliked again. **Clip Shares**: When disabled, users can share clips and see count updates, but if they swipe away and return, the original count will be displayed. Example configuration: ```kotlin Storyteller.initialize( apiKey = "[APIKEY]", userInput = StorytellerUserInput("unique-user-id"), eventTrackingOptions = StorytellerEventTrackingOptions( enablePersonalization = true, enableStorytellerTracking = true, enableUserActivityTracking = true, enableAdTracking = true, enableFullVideoAnalytics = true, enableRemoteViewingStore = true, disabledFunctionalFeatures = listOf( StorytellerDisabledFunctionalFeature.ClipLikes, StorytellerDisabledFunctionalFeature.ClipViewedStatus ) ) ) ``` ## enableEventTracking/disableEventTracking (Legacy) > Note: This method will be removed in version 11.0.0. Please use `eventTrackingOptions` instead. The `disableEventTracking()` method will disable storing analytics events on Storyteller servers. By default, event tracking is enabled. Note that some events are necessary for user functionality and will still be made (but not stored) when event tracking is disabled. Event tracking can be enabled again by calling `enableEventTracking()` Example: ```kotlin Storyteller.enableEventTracking() ``` ```kotlin Storyteller.disableEventTracking() ``` --- ## Source: StorytellerLists.md # Storyteller Jetpack Compose SDK This is a developers' guide for setting up Storyteller Compose SDK for native Android apps. This guide will cover the basic technical steps for integrating Storyteller Compose SDK into your app. _Note: While this guide focuses on the Jetpack Compose implementation, corresponding traditional Android Views (e.g., `StorytellerStoriesRowView`, `StorytellerStoriesGridView`, `StorytellerClipsRowView`, `StorytellerClipsGridView`) also exist and can be used in XML layouts or programmatically. Their configuration attributes and delegate usage are similar to their Compose counterparts._ ## Getting Started ### Resources - [Storyteller Showcase App (Compose)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/main/src/main/java/com/getstoryteller/storytellershowcaseapp/ShowcaseApp.kt#L24) ### Showcase examples - [Lists + Cards feed (`HomeScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/HomeScreen.kt#L164) - [Storyteller list rendering (`StorytellerItem`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/storyteller/StorytellerItem.kt#L58) ### How to Add the SDK to Your Project Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`. #### SDK Installation The Compose SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/). > Note: Starting from 9.3.0 SDK version Compose is now part of the SDK and there is no need to add separate Compose dependency. The steps to add the SDK are [in the Getting Started documentation](GettingStarted.md) #### Usage 1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc. ```kotlin @Composable fun MainScreen() { Box() { // ... } } ``` 1. Storyteller Composables Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK. ```kotlin StorytellerStoriesRow( modifier = Modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below. delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below. state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below. ) ``` We have the following **composable types** available: - StorytellerStoriesRow - StorytellerStoriesGrid - StorytellerClipsRow - StorytellerClipsGrid 1. Using the Storyteller Composables You can always modify your Composables during recomposition. Reload Data: Just like normal composable lists, they have a list state, so as Storyteller lists, they have a state, and we will use it to reload the data. There is an extra parameter `state` in the Composables, which is set by default. Let's see how it's used: ```kotlin val state = rememberStorytellerRowState() // or rememberStorytellerGridState() for StorytellerStoriesGrid or StorytellerClipsGrid StorytellerStoriesRow( // ... state = state ) // At any point in your code you can call `reloadData` method, maybe after doing PTR or after some event. LaunchedEffect(Unit) { state.reloadData() } ``` 1. Setting categories and collections, applying themes and styles You can use `StorytellerStoriesDataModel` object for configuring your Stories Composables. It has the following properties: ```kotlin StorytellerStoriesDataModel( categories = listOf("category1", "category2"), // list of stories categories for the Composable. If you want to use all categories, you can pass an empty list. theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here. uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app. displayLimit = 6, // a limit of the stories to be displayed in the Composable. Default is Int.MAX_VALUE. cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND. visibleTiles = null, // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size. context = mapOf("placementId" to "home_stories", "location" to "Home"), // Optional analytics context containing integrator-defined key-value pairs. See [Analytics](Analytics.md#context) for more details. ) ``` > Note: `StorytellerStoriesGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`. You can use `StorytellerClipsDataModel` object for configuring your Clips Composables. It has the following properties: ```kotlin StorytellerClipsDataModel( collection = "collection1", // a string representing your collection of Clips. theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here. uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app. displayLimit = 6, // a limit of the clips to be displayed in the Composable. Default is Int.MAX_VALUE. cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND. visibleTiles = null, // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size. context = mapOf("placementId" to "home_clips", "location" to "Home"), // Optional analytics context containing integrator-defined key-value pairs. See [Analytics](Analytics.md#context) for more details. ) ``` > Note: `StorytellerClipsGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`. ## Storyteller Compose ListView Delegate The `StorytellerListViewDelegate` will provide you necessary callbacks for the state of data inside you Composables. This delegate is used to track when data load starts, when it completes, when a tile is tapped, and when player was dismissed by user. We recommend using `remember` for the delegate's object to avoid creating new ones during recomposition. ```kotlin val listViewDelegate by remember("your_item_id") { val value = object : StorytellerListViewDelegate { override fun onPlayerDismissed() { // handle player dismissed event } override fun onDataLoadStarted() { // handle loading started event } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int) { // handle data load complete event } override fun onTileTapped(tileType: StorytellerTileType) { // handle tile tap event - called before the player Activity is opened // tileType can be either StorytellerTileType.Story or StorytellerTileType.Clip } } mutableStateOf( value ) } StorytellerStoriesRow( delegate = listViewDelegate, // ... ) ``` ### StorytellerTileType The `StorytellerTileType` is a sealed class that represents the type of tile that was tapped. It contains information about the tapped item and can be one of two types: #### StorytellerTileType.Story Represents a Story tile that was tapped. **Properties:** - `id: String` - The unique identifier of the story - `categories: List` - List of categories that this story belongs to #### StorytellerTileType.Clip Represents a Clip tile that was tapped. **Properties:** - `id: String` - The unique identifier of the clip - `collectionId: String` - The collection ID that this clip belongs to - `categories: List` - List of categories that this clip belongs to **Example usage:** ```kotlin override fun onTileTapped(tileType: StorytellerTileType) { when (tileType) { is StorytellerTileType.Story -> { val storyId = tileType.id val categories = tileType.categories // Handle story tile tap Log.d("TileTap", "Story tapped: $storyId, categories: $categories") } is StorytellerTileType.Clip -> { val clipId = tileType.id val collectionId = tileType.collectionId val categories = tileType.categories // Handle clip tile tap Log.d("TileTap", "Clip tapped: $clipId from collection: $collectionId, categories: $categories") } } } ``` ## StorytellerStoriesGrid The `StorytellerStoriesGrid` is a Composable with same functionality as the `StorytellerStoriesGridView` view from the Storyteller SDK. ### Usage in Compose ```kotlin //Stories Grid StorytellerStoriesGrid( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerGridState(), isEnabled = true ) //Stories Scrollable Grid StorytellerStoriesGrid( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = true, isEnabled = true ) ``` ### Parameters - `modifier` - a `Modifier` object that will be applied to the `StorytellerStoriesGrid` composable. - `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event. - `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state. + the `rememberStorytellerGridState()` has two parameters: - `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid. - `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable. - `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it wil layout all tiles in the grid. - `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled. ## StorytellerStoriesRow The `StorytellerStoriesRow` is a Composable with same functionality as the `StorytellerStoriesRowView` view from the Storyteller SDK. ### Usage in Compose ```kotlin StorytellerStoriesRow( modifier = modifier, dataModel = StorytellerStoriesDataModel(categories = emptyList()), delegate = listViewDelegate, state = rememberStorytellerRowState() ) ``` ## StorytellerClipsGrid The `StorytellerClipsGridView` is a Composable with same functionality as the `StorytellerClipsGridView` view from the Storyteller SDK. ### Usage in Compose ```kotlin //Clips Grid StorytellerClipsGrid( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = false, isEnabled = true ) //Clips Scrollable Grid StorytellerClipsGrid( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerGridState(), isScrollable = true, isEnabled = true ) ``` ### Parameters - `modifier` - a `Modifier` object that will be applied to the `StorytellerClipsGrid` composable. - `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event. - `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state. + the `rememberStorytellerGridState()` has two parameters: - `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid. - `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable. - `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it will layout all tiles in the grid. - `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled. - `searchInput` - an optional `SearchInput` object that can be used to filter clips in the grid. - `isVisible` - a `Boolean` flag that indicates if the grid should be visible. Defaults to true. ## StorytellerClipsRow The `StorytellerClipsRow` is a Composable with same functionality as the `StorytellerClipsRowView` view from the Storyteller SDK. ### Usage in Compose ```kotlin StorytellerClipsRow( modifier = modifier, dataModel = StorytellerClipsDataModel(collection = "collection"), delegate = listViewDelegate, state = rememberStorytellerRowState() ) ``` --- ## Source: StorytellerListViews.md # What is StorytellerListView (Legacy) `StorytellerListView` is a base abstract class for views displaying lists of Stories or Clips. Storyteller Views are considered legacy and support may be removed in the near future. Please prefer using [Composables instead](StorytellerLists.md). ## Showcase examples - [XML — lists screen (`DashboardFragment`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/DashboardFragment.kt#L30) - [XML — RecyclerView ViewHolders (`ViewHolders`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/adapter/ViewHolders.kt#L38) The Storyteller SDK offers two implementations of this abstract class: - `StorytellerStoriesView` - `StorytellerClipsView` Each of these classes has their corresponding rows and grid views as the following: For Stories - `StorytellerStoriesRowView` - `StorytellerStoriesGridView` For Clips - `StorytellerClipsRowView` - `StorytellerClipsGridView` This abstract class contains all abstract attributes and methods that are common to all `StorytellerListView` implementations. ## How to improve the performance of RecyclerView? If you use the `StorytellerViews` in a `RecyclerView`, using the `setViewHolderParent` method is not mandatory but can significantly improve the UI performance. Inside the `RecyclerView.Adapter`: ```kotlin ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.my_item, parent, false) return MyViewHolder(itemView, parent) } ... ``` Inside the `RecyclerView.ViewHolder`: ```kotlin class MyViewHolder( private val view: View, private val parent: ViewGroup, ): RecyclerView.ViewHolder(view) { private val binding = ListStoryRowBinding.bind(view) fun onBind(item: Item) { binding.storytellerView.setViewHolderParent(parent) ... } } ``` ## Adding a StorytellerStoriesRowView The `StorytellerStoriesRowView` can be added to your app using XML layout or in code. ### XML Add a `com.storyteller.ui.list.StorytellerStoriesRowView` element to your layout ```xml ``` ### Code 1. Create a `StorytellerStoriesRowView` instance ```kotlin val storyRowView = StorytellerStoriesRowView(context) ``` 1. Add the created view instance to the view hierarchy ```kotlin view.addView(storyRowView) ``` ### Recommendations for `StorytellerListView` Hosting Activity Please keep in mind that Stories will open in the activity that displays a status bar only if there is a cutout notch present on the device. In other cases, Stories will open in the activity without a status bar. Status bar visibility changes can have an impact on shared element transitions between Storyteller List View and Story Viewing Activity. For that reason, we recommend hosting activities to use LAYOUT_STABLE flags as in the code snippet below ```kotlin // Storyteller hosting activity onCreate method override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ~~~~~~~ window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // ~~~~~~~ } ``` ## Further Reading ### Additional Storyteller Methods `Storyteller` has some helper methods which may be useful, see [AdditionalMethods](AdditionalMethods.md) for more details. ### Implementing Storyteller Callbacks `StorytellerDelegate` has callbacks for events which can be implemented, see [StorytellerDelegate](StorytellerDelegates.md) for more details. ### Implementing StorytellerListView Callbacks `StorytellerListView` has callbacks for events which can be implemented, see [StorytellerListViewDelegate](StorytellerDelegates.md) for more details. ## Configuring a StorytellerStoriesView ### Attributes This sections describes attributes common to `StorytellerStoriesRowView` and `StorytellerStoriesGridView`. Notes are used to mark if there is a difference in property interpretation. #### delegate The `delegate` is the `StorytellerListViewDelegate` instance for `StorytellerListView` callbacks (see [Implementing StorytellerListViewDelegate methods](StorytellerDelegates.md)). #### StorytellerStoriesView.ListConfiguration The `ListConfiguration` class is used to configure the `StorytellerStoriesRowView` and `StorytellerStoriesGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`. Usage: ```kotlin val storytellerStoriesRowView = StorytellerStoriesRowView(context) storytellerStoriesRowView.configuration = StorytellerStoriesView.ListConfiguration( theme = customTheme, uiStyle = StorytellerListViewStyle.AUTO, displayLimit = 10, cellType = StorytellerListViewCellType.SQUARE, categories = listOf("category1", "category2", "categoryX"), context = mapOf("placementId" to "home_stories", "location" to "Home"), ) ``` ##### cellType The `cellType` is the style of the cell. This can either be `ROUND(0)` or `SQUARE(1)`. The default value is `SQUARE(1)`. ##### categories The `categories` attribute is a list of Categories that the list view should show to the user. The default value is an empty list. The `categories` property can be used to show specific Stories content in the row or grid by supplying a list of valid Categories as strings. Categories can be defined in the CMS. If no Categories are assigned then the content from the default or "Home" Category is displayed. ##### theme The `theme` parameter is used to set the [Theme](Themes.md) for the `StorytellerListView`. If theme is set to null, then theme set as`Storyteller.theme` global property is used. The theme determines how the items within the List View are presented as well as various features of the player once it is launched. ##### uiStyle The `uiStyle` adjusts whether Storyteller renders in light mode, dark mode or follows the system setting. `uiStyle` takes the following values: - `StorytellerListViewStyle.AUTO` - default value, the StorytellerListView will adjust its color scheme automatically according to the current system UI mode - `StorytellerListViewStyle.LIGHT` - force the StorytellerListView to use the light theme - `StorytellerListViewStyle.DARK` - force the StorytellerListView to use the dark theme ##### displayLimit The `displayLimit` is the maximum amount of tiles that can be shown in the list. ##### context The `context` parameter is an optional `StorytellerAnalyticsContext` (which is a `Map`) containing integrator-defined key-value pairs for analytics attribution. This context will be included with all analytics events originating from this UI instance. See [Analytics](Analytics.md#context) for more details. ##### visibleTiles The `visibleTiles` parameter provides control over row tile sizing behavior. **visibleTiles** (`Float?`, default: `null`) Controls how many tiles are visible in the row viewport. For example, a value of `2.5f` shows two full tiles and half of a third tile. The row height is automatically calculated based on: - The number of visible tiles - The screen width - The tile type (round or square) ```kotlin storytellerStoriesRowView.configuration = StorytellerStoriesView.ListConfiguration( visibleTiles = 3.5f, // Show 3.5 tiles in the viewport categories = listOf("category1"), ) ``` #### Automatic Mode (default) When `visibleTiles` is `null` (the default), the row uses automatic height calculation that scales with the system font size for improved accessibility: ```kotlin storytellerStoriesRowView.configuration = StorytellerStoriesView.ListConfiguration( visibleTiles = null, // Automatic mode (default) categories = listOf("category1"), // Row height scales with device font size (Settings → Display → Font size) ) ``` **Behavior Summary:** | visibleTiles | Mode | Behavior | |-------------|------|----------| | `null` | **AUTOMATIC** (default) | Row height scales with system font size (base: 150dp × font scale, capped 0.5x-2.5x). Provides the best accessibility for users who adjust their device's font size. | | `2.5f` | **DYNAMIC** | Shows approximately 2.5 tiles in the viewport. Row height is calculated based on screen width and tile aspect ratio. For round tiles with many visible tiles (3+), the height automatically adjusts to prevent title text from being cut off. | **Notes:** - Grid views always use fixed aspect ratio sizing and ignore this parameter - AUTOMATIC mode (default) provides the best accessibility for users with visual impairments who adjust their device's font size in system settings - DYNAMIC mode is ideal when you want precise control over how many tiles are shown in the viewport - Row views only - grid views ignore the `visibleTiles` parameter #### Dynamic Scaling Item tiles will scale dynamically to fit the row or grid view. The base size of the tiles are: - 100dp width x 150dp height for square tiles - 76dp width x 105dp height for round tiles The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents. In general, tiles will maintain its base proportion and scale up or down to meet view hierarchy constrains. For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations. ##### Example 1 ```xml ``` The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp` ##### Example 2 ```xml ``` The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerStoriesRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly. ##### Example 3 ```xml ``` In this case, the final item tile size will be the height of the window. This happens because the `StorytellerStoriesRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also. ##### Example 4 ```xml ... ... ``` In this case, the final item tile size will be 100dp x 150dp (the base size). Since the `StorytellerStoriesRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size 100dp x 150dp (base size). #### XML Attributes can also be applied in XML. ```xml ``` ### Methods #### reloadData The `reloadData` method starts loading fresh data for all Stories from the API. On completion, it updates the Story data, starts prefetching content and updates the read status of the Stories. The `onDataLoadStarted` and `onDataLoadComplete` methods on the [StorytellerListViewDelegate](StorytellerDelegates.md) are called accordingly (the latter with appropriate data depending on the result of the API requests). ```kotlin val storytellerStoriesRowView = StorytellerStoriesRowView(context) storytellerStoriesRowView.reloadData() ``` ## Configuring a StorytellerClipsView ### Attributes This sections describes attributes common to `StorytellerClipsRowView` and `StorytellerClipsGridView`. Notes are used to mark if there is a difference in property interpretation #### StorytellerListViewDelegate - `delegate`: the `StorytellerListViewDelegate` instance for `StorytellerClipsView` callbacks (see [Implementing StorytellerListViewDelegate methods](StorytellerDelegates.md)) #### StorytellerClipsView.ListConfiguration The `ListConfiguration` class is used to configure the `StorytellerClipsRowView` and `StorytellerClipsGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`. `StorytellerClipsView.ListConfiguration` class attributes: Example: ```kotlin val storytellerClipsRowView = StorytellerClipRowView(context) storytellerClipsRowView.configuration = StorytellerClipsView.ListConfiguration( theme = customTheme, uiStyle = StorytellerListViewStyle.AUTO, displayLimit = 10, cellType = StorytellerListViewCellType.SQUARE, collection = "collectionId", context = mapOf("placementId" to "home_clips", "location" to "Home"), ) ``` - `collection`: the Collection that the list view should show to the user. The default value is `null`.The `collection` property is used to show specific Clips content in the row or grid by supplying a single Collection as string. Collections can be defined in the CMS. If no Collection is assigned, then no Clips content is displayed. - `theme`: This parameter is used to set the [Theme](Themes.md) for the `StorytellerClipsView`. If theme is set to `null`, then theme set as`Storyteller.theme` global property is used. The theme determines how the items within the List View are presented as well as various features of the player once it is launched. - `displayLimit`: only display up to this number of tiles in the list. - `uiStyle`: adjust whether Storyteller renders in light mode, dark mode or follows the system setting. `uiStyle` takes the following values: - `StorytellerListViewStyle.AUTO` - default value, the StorytellerClipsView will adjust its color scheme automatically according to the current system UI mode - `StorytellerListViewStyle.LIGHT` - force the StorytellerClipsView to use the light theme - `StorytellerListViewStyle.DARK` - force the StorytellerClipsView to use the dark theme - `context`: Optional `StorytellerAnalyticsContext` (a `Map`) containing integrator-defined key-value pairs for analytics attribution. This context will be included with all analytics events originating from this UI instance. See [Analytics](Analytics.md#context) for more details. #### Feed Title The feed title in the Clips player can be configured in the CMS for the Collection. This can be a custom title or image. ### Dynamic Scaling Item tiles will scale dynamically to fit the row or grid view. The base size of the tiles are: - 100dp width x 150dp height for square tiles - 76dp width x 105dp height for round tiles The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents. In general, tiles will maintain its base proportions and scale up or down to meet view hierarchy constrains. For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations. #### Example 1 ```xml ``` The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp` #### Example 2 ```xml ``` The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly. #### Example 3 ```xml ``` In this case, the final item tile size will be the height of the window. This happens because the `StorytellerRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also. #### Example 4 ```xml ... ... ``` In this case, the final item tile size will be `100dp x 150dp` (the base size). Since the `StorytellerRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size `100dp x 150dp` (base size). ### XML Views can also be applied in XML. ```xml ``` ### Methods #### reloadData The `reloadData` method starts loading fresh data for all Clips from the API. On completion, it updates the Clips data, starts prefetching content and updates the read status of the Clips. The `onDataLoadStarted` and `onDataLoadComplete` methods on the [StorytellerListViewDelegate](StorytellerDelegates.md) are called accordingly (the latter with appropriate data depending on the result of the API requests). ```kotlin val storytellerClipsRowView = StorytellerClipsRowView(context) storytellerClipsRowView.reloadData() ``` --- ## Source: StorytellerClipsFragment.md # StorytellerClipsFragment `StorytellerClipsFragment` is a fragment that can be used to embed a clip in your own activities. Like a regular Android Fragment, it can be configured from an XML layout or instantiated programmatically and attached to the host's (another fragment or activity) FragmentManager via a fragment transaction. > Clips are delivered in a 9:16 aspect ratio. While `StorytellerClipsFragment` will do content > cropping in a way that all given space is taken by the content (all area is consumed, cropping is > applied to avoid content distortion). If you want to present content without > cropping, `StorytellerClipsFragment` should reside > in a container that maintains a 9:16 aspect ratio. ## Showcase examples - [Compose — embedded clips (`MomentsScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/moments/MomentsScreen.kt#L160) - [XML — embedded clips (`EmbeddedClipFragment`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/embedded/EmbeddedClipFragment.kt#L74) ## Using StorytellerClipsFragment from xml layout `StorytellerClipsFragment`can be used directly from the layout XML files. The usage is identical to typical Android fragments. You need to specify the fully qualified fragment class name in the android:name property and the collection ID using app:storyteller_collection_id_property. Note that in this example, a constraint is applied to the dimensions' proportions so the clip content is not cropped at all. app:storyteller_initial_category is optional and can be used to set the initial category of the collection to be viewed. If this is used. The clip will be first loaded with the collection. The selected category will then be navigated to automatically. ```xml ``` ## Using StorytellerClipsFragment programmatically `StorytellerClipsFragment` can also be used programmatically. To embed a clip fragment programmatically, you need to follow these steps: 1. Create a new fragment instance using the `StorytellerClipsFragment.create(collectionId: String, context: StorytellerAnalyticsContext)` method. a. Optionally, you can also set the `initialCategory` property to a string value to automatically navigate to a selected category. 2. Create a fragment transaction that would add this fragment to the fragment container view. 3. Commit the created transaction. See the following snippet illustrating attaching a fragment in the `Activity.onCreate` method: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // An example using viewBinding feature; see view binding for reference. val binding = ActivityClipFragmentHostBinding.inflate(layoutInflater) setContentView(binding.root) val storytellerClipsFragment = StorytellerClipsFragment.create( collectionId = "yourCollectionId", context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val transaction = supportFragmentManager.beginTransaction() transaction.add( binding.fragmentHost.id, storytellerClipsFragment ) transaction.commit() } ``` ## Controlling playback in StorytellerClipsFragment `StorytellerClipsFragment` by default starts playback as soon as it is attached to the host and visible. Standard Android Fragment lifecycle events typically handle automatic pausing and resuming (e.g., when the app goes to the background), complementing the manual control provided by the `shouldPlay` property. ```kotlin // finding the fragment by id using the fragment manager val storytellerClipsFragment = supportFragmentManager.findFragmentById(binding.fragmentHost.id) as StorytellerClipsFragment storytellerClipsFragment.shouldPlay = false // stops playback storytellerClipsFragment.shouldPlay = true // starts playback ``` `StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val canGoBack = storytellerClipsFragment.canGoBack ``` `StorytellerClipsFragment` also contains `listener` property which can be used to control playback and handle top level back button press. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped / host should handle back } override fun onDataLoadStarted() { // will be invoked when data load starts } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){ // will be invoked when data load completes or fails } } storytellerClipsFragment.reloadData() ``` ## External back button handling `StorytellerClipsFragment` does not show the top level back button by default. You can control the behaviour by setting `topLevelBackEnabled` property and setting `onTopLevelBackPressed` according to your needs. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped } } ``` `StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) val canGoBack = storytellerClipsFragment.canGoBack ``` ## Initial Category To set the `StorytellerClipsFragment` to start with a specific category, you can set the `initialCategory` property. Programmatical Usage ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", initialCategory = "yourCategory", context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) ``` XML Usage ```xml ``` If the category is found in the collection, the clip will be loaded with the collection and the selected category. The category will be navigated to automatically. If the category is not found in the collection or is invalid, it will be ignored. ## Reload Data `StorytellerClipsFragment` in order to reload data you can call `reloadData()` method. This method will make a request to the backend to fetch the latest data. If this method is called when there are category filters applied, then it will go back one level. If there are no category filters applied this method will reload data. Loading state can be observed by setting `listener` property and overriding it's `onDataLoadStart` and `onDataLoadComplete` methods. ```kotlin val storytellerClipsFragment = StorytellerClipsFragment.create( collection = "yourCollectionId", topLevelBackEnabled = true, context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen") ) storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener { override fun onTopLevelBackPressed(): Boolean { // will be invoked when top level back button is pressed return true // true if fragment needs to be stopped / host should handle back } override fun onDataLoadStarted() { // will be invoked when data load starts } override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){ // will be invoked when data load completes or fails } } storytellerClipsFragment.reloadData() ``` ## Inset Management One of the usecase of `StorytellerClipsFragment` is to embed it in a view that is laid behind status bar. While that will increase size of the screen available for the content, this arrangement can potentially causing the clip title and other UI elements to be obscured by the status bar. To avoid this problem you can set `StorytellerClipsFragment.topInset` property to the height of the status bar. This will cause the fragment to offset the title and the top buttons by the given amount of pixels. Similarly if the bottom part of the view is obscured by the Navbar, you can set `StorytellerClipsFragment.bottomInset` property to the height of the navigation bar. The recommended approach is to use this property together with `onApplyWindowInsets` callback. ```kotlin ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, winInsets -> val inset = winInsets.getInsets(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) storytellerClipsFragment.topInset = inset.top storytellerClipsFragment.bottomInset = inset.bottom WindowInsetsCompat.CONSUMED } ``` ## Compose Integration `StorytellerClipsFragment` can be used in Jetpack Compose using the `StorytellerEmbeddedClips` composable. ```kotlin class DemoComposeEmbeddedClipsActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val collection = intent.getStringExtra("collection") ?: "" val widthPercentage = intent.getIntExtra("width", 100) / 100F val heightPercentage = intent.getIntExtra("height", 80) / 100F val topLevelBack = intent.getBooleanExtra("topLevelBack", false) val initialCategory = intent.getStringExtra("category") ?: "" enableEdgeToEdge() setContent { val state = rememberStorytellerEmbeddedClipsState( collectionId = collection, topLevelBack = topLevelBack, initialCategory = initialCategory, context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) StorytellerEmbeddedClips( modifier = Modifier .fillMaxWidth(widthPercentage) .fillMaxHeight(heightPercentage), state = state, ) } } companion object { fun start(context: Context, collection: String, category: String?, width: Int, height: Int, topLevelBack: Boolean) { // add params to intent Intent(context, DemoComposeEmbeddedClipsActivity::class.java).apply { putExtra("collection", collection) putExtra("category", intialCategory) putExtra("width", width) putExtra("height", height) putExtra("topLevelBack", topLevelBack) context.startActivity(this) } } } } ``` ### StorytellerEmbeddedClipsState `rememberStorytellerEmbeddedClipsState` is a composable function that creates a `StorytellerEmbeddedClipsState` object that holds the state of the `StorytellerEmbeddedClips` composable. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = "collection", topLevelBack = topLevelBack, context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) StorytellerEmbeddedClips( modifier = Modifier, state = state ) ``` `StorytellerEmbeddedClipsState` contains `canGoBack` property that can be used to check if the fragment can go back from the current Category or is it at the top level. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = "collection", topLevelBack = topLevelBack, context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) val canGoBack = state.canGoBack // true if user can navigate back ``` `StorytellerEmbeddedClipsState` contains `goBack()` which will move the content to previous Category if the user is not at the top level. ```kotlin val state = rememberStorytellerEmbeddedClipsState( collectionId = collection, topLevelBack = topLevelBack, context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen") ) state.goBack() // navigate to previous category programmatically ``` --- ## Source: StorytellerHome.md # Storyteller Home `StorytellerHome` is a component which allows multiple Stories and Clips Rows or Grids to be embedded in a single screen in your application in a list. ## How to Use The code samples below show how to use the `StorytellerHome` component in both `Compose` and `Fragment`. It includes a pull-to-refresh (PTR) mechanism, but you can also use the `reloadData` method to refresh data on your side. *Defaults: If `theme` is not provided, the global theme set via `Storyteller.theme` will be used. If `uiStyle` is not provided, it defaults to `StorytellerListViewStyle.AUTO`, which follows the system's light/dark mode setting.* `context` - optional context data that will be included in analytics callbacks for attribution. This allows you to track which sources drive engagement with your home content. See [Analytics](Analytics.md#context) for more details. ### Compose ```kotlin // without reload data outside sdk StorytellerHome( homeId = "home", theme = yourTheme, uiStyle = StorytellerListViewStyle.AUTO, context = mapOf("placementId" to "home_screen", "location" to "Home"), modifier = Modifier ) // with reload data outside sdk val state = rememberStorytellerHomeState() StorytellerHome ( homeId = "home", theme = yourTheme, uiStyle = StorytellerListViewStyle.AUTO, context = mapOf("placementId" to "home_screen", "location" to "Home"), // Optional analytics context state = state, modifier = Modifier ) // ... // call for reload data outside sdk suspend fun reloadData() { state.reloadData() } ``` ### Fragment ```kotlin val fragment = StorytellerHomeFragment.create( homeId = "home", context = mapOf("placementId" to "home_screen", "location" to "Home"), // Optional analytics context // theme = yourTheme, // Optional: Defaults to Storyteller.theme // uiStyle = StorytellerListViewStyle.AUTO, // Optional: Defaults to AUTO ) // call for reload data outside sdk fragment.reloadData() ``` --- ## Source: Cards.md # Storyteller Cards Storyteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title and subtitle. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user. ## Showcase examples - [Compose — rendering Cards (`HomeScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/HomeScreen.kt#L202) ## Usage You can integrate Storyteller Cards into your app using Jetpack Compose. ### Jetpack Compose For Jetpack Compose, use the `StorytellerCard` composable. 1. **Data Model**: Create a `StorytellerCardDataModel` object, specifying the `collectionId` for the Card collection you want to display. You can also provide optional `context` data for analytics. When configured, `context` will be included in all analytics events when users interact with the Card. See [Analytics](Analytics.md#context) for more details. 2. **State**: Initialize a `StorytellerCardState` using `rememberStorytellerCardState()` to manage the card's state and reload functionality. 3. **View**: Create the `StorytellerCard` composable, passing in the `StorytellerCardDataModel` instance and state. 4. **Delegate (Optional)**: Provide an optional `StorytellerCardViewDelegate` to handle events like `onDataLoadComplete`. This allows you to react to data loading success or failure (e.g., by hiding the component). ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.storyteller.ui.compose.components.cards.StorytellerCard import com.storyteller.ui.compose.components.cards.StorytellerCardDataModel import com.storyteller.ui.compose.components.cards.StorytellerCardViewDelegate import com.storyteller.ui.compose.components.cards.rememberStorytellerCardState import com.storyteller.domain.entities.StorytellerError @Composable fun MyCardSection() { val dataModel = StorytellerCardDataModel( collectionId = "card-collection-id", context = mapOf("placementId" to "home_card", "location" to "Home") ) val cardState = rememberStorytellerCardState(dataModel.collectionId) val cardViewDelegate = remember { object : StorytellerCardViewDelegate { override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (!success) { // Example: update your UI state here (e.g., hide this section on failure). } } } } StorytellerCard( dataModel = dataModel, state = cardState, delegate = cardViewDelegate, modifier = Modifier ) // Optional: Add a reload button Button(onClick = { cardState.reloadData() }) { Text("Reload Card") } } ``` ## Reloading The `StorytellerCardState` provides a `reloadData()` method. Call this method to manually trigger a refresh of the Card data from the server. ## Viewed/Tapped Ordering In the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content. ## Theming Card appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection. The following properties can be configured in the CMS and influence the Card's presentation and behaviour: - `style.textLengthMode` (default: `truncate`): How text that exceeds the available space is handled. + `truncate`: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit. + `resize`: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading). - `style.textAlignment` (default: `start`): Horizontal alignment of the heading and subheading. Can be `start`, `center`, or `end`. - `style.padding` (default: `12`): Inner padding around the text content. + For full-bleed cards (`marginHorizontal = 0`) with text _below_ the image and _all_ cards with text _on_ the image, padding is applied to all sides of the text. + For cards with text _below_ the image where `marginHorizontal > 0`, padding is applied only to the top and bottom of the text. - `style.marginHorizontal` (default: `0`): Horizontal margin around the card. `0` means full-bleed. - `style.cornerRadius` (default: `{theme.primitives.cornerRadius}`): Corner radius of the card. The application depends on `marginHorizontal` and text position. + Not applied for full-bleed cards (`marginHorizontal=0`) with text _below_ the image. + Applied to the _image_ for cards with text _below_ the image and `marginHorizontal > 0`. + Applied to the _whole card_ for cards with text _on_ the image and `marginHorizontal > 0`. - `style.headingsSpacing` (default: `3`): Vertical spacing between the heading and subheading. - `style.dynamicTypeEnabled` (default: `true`): Whether cards typography participates in Android Dynamic Type scaling. When `false`, cards keep fixed text sizes, line heights, and letter spacing regardless of system font scale. - `style.backgroundColorLight` (optional): Light-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`). - `style.backgroundColorDark` (optional): Dark-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`). - `style.heading.font` (default: `{theme.customFont}`): Font family for the heading. - `style.heading.textSize` (default: `22`): Font size for the heading. - `style.heading.lineHeight` (default: `28`): Line height for the heading. - `style.heading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.heading.letterSpacing` (default: `0`): Letter spacing for the heading. - `style.heading.textColor` (default: `{theme.colors.white.primary}`): Text color for the heading when text is displayed _on_ the background asset. - `style.heading.textBelowContentColorLight` (optional): Light-mode heading text color override when `textOverContent = false`. - `style.heading.textBelowContentColorDark` (optional): Dark-mode heading text color override when `textOverContent = false`. - `style.subHeading.font` (default: `{theme.customFont}`): Font family for the subheading. - `style.subHeading.textSize` (default: `16`): Font size for the subheading. - `style.subHeading.lineHeight` (default: `21`): Line height for the subheading. - `style.subHeading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`). - `style.subHeading.letterSpacing` (default: `0`): Letter spacing for the subheading. - `style.subHeading.textColor` (default: `{theme.colors.white.secondary}`): Text color for the subheading when text is displayed _on_ the background asset. - `style.subHeading.textBelowContentColorLight` (optional): Light-mode subheading text color override when `textOverContent = false`. - `style.subHeading.textBelowContentColorDark` (optional): Dark-mode subheading text color override when `textOverContent = false`. - `behavior.reloading.reloadOnExit` (default: `true`): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player). - `behavior.reloading.reloadOnForeground` (default: `true`): Whether the Card Collection reloads when the app comes to the foreground. ### Button Behavior - **Button positioning**: Buttons are optional visual elements that follow the `textOverContent` property: + When `textOverContent = true`: Button appears on the card (overlaying the content), positioned below the title/subtitle + When `textOverContent = false`: Button appears below the card (below the title/subtitle section) - **Button functionality**: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped - **Button text**: The button text is defined in the Card data, not the theme ### Button Theme Properties - `style.button.title.font` (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height. - `style.button.title.textSize` (default: `16`): Font size for the button text. - `style.button.title.lineHeight` (default: `21`): Line height for the button text. - `style.button.title.textCase` (default: `default`): Text case transformation for the button text (`upper`, `lower`, `default`). - `style.button.title.letterSpacing` (default: `0`): Letter spacing for the button text. - `style.button.title.textColor` (default: `{theme.colors.white.primary}`): Text color for the button when text is displayed _on_ the background asset. - `style.button.title.textBelowContentColorLight` (optional): Light-mode button text color override when `textOverContent = false`. - `style.button.title.textBelowContentColorDark` (optional): Dark-mode button text color override when `textOverContent = false`. - `style.button.backgroundColor` (optional): Background color of the button. If not set, the button will have a transparent background with an outline. - `style.button.outlineColor` (default: `{theme.colors.white.primary}` for text on image, otherwise the resolved button title below-content color when present, falling back to `{theme.colors.black.primary}` in light mode and `{theme.colors.white.primary}` in dark mode): Color of the button outline/border. - `style.button.outlineWidth` (default: `1`): Width of the button outline/border in points. - `style.button.cornerRadius` (optional, default: `{theme.primitives.cornerRadius}`): Corner radius of the button. If null or not set, falls back to the theme's default corner radius. - `style.button.textAlignment` (default: uses card `textAlignment`): Text alignment for the button text. If not specified or null, uses the card's text alignment setting. --- ## Source: OpenPlayer.md # Open Player ## Showcase examples - [Compose — opening a category (`ImageActionItem`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/ImageActionItem.kt#L60) ## Open Stories This call opens a Story with a given ID. The call will only open that individual Story. ```kotlin fun openStory(activity: Activity, storyId: String? = null, onError: (StorytellerError) -> Unit = {}) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `storyId` - this is a Story's ID, if it is `null` then `onError` will be called - `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available) ## Open Pages This call opens a Page with a given ID. The call will only open that individual Story containing that page. ```kotlin fun openPage(activity: Activity, pageId: String? = null, onError: (StorytellerError) -> Unit = {}) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `pageId` - this is a Page's ID, if it is `null` then `onError` will be called - `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available) ## Open Collection ```kotlin fun openCollection( activity: Activity, configuration: StorytellerClipCollectionConfiguration, titleDrawable: Drawable? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `configuration` - this is a Clip Collection configuration, see below - `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional) - `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available) `StorytellerClipCollectionConfiguration` parameters: - `collectionId` - this is a Clip Collection's ID (required) - `categoryId` - this is a Category ID (optional), if provided, this category will be opened, if the category is not available or not found it will be ignored - `clipId` - this is a Clip ID to open (optional) ## Open Category This call opens a Story category. If Story ID is not supplied, the first Story in the collection will be opened. ```kotlin fun openCategory( activity: Activity, category: String, storyId: String? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `category` - this is a Story category - `storyId` - this is a Story ID (optional) - `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available) ### openStoryByExternalId This call opens a Story by external ID. ```kotlin fun openStoryByExternalId( context: Activity, externalId: String? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `externalId` - external Id to open a Story - `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available) ### openCollectionByExternalId This call opens a Collection of Clips and shows Clip with external id. If Clip ID is not supplied, the first Clip in the collection will be opened. ```kotlin fun openCollectionByExternalId( activity: Activity, collectionId: String, externalId: String? = null, titleDrawable: Drawable? = null, onError: (StorytellerError) -> Unit = {} ) ``` Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Player - `collectionId` - this is a Clip Collection's ID - `externalId` - this is a Clip's external ID (optional) - `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional) - `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available) --- ## Source: Search.md # Search The `Search` component allows users to search for Storyteller clips and stories. As users type, a list of suggestions will appear, from which users can either select a suggestion or search using their entered term. Results are categorized into two sections: `Stories` and `Clips`, based on their type. Filters allow users to narrow down their search results, enabling them to find specific content that meets their criteria more efficiently. For instance, users can apply filters such as date posted, content type or they can sort it by certain criteria. ## Showcase examples - [Compose — opening Search (`MainScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/main/MainScreen.kt#L75) ## Search Filters `Date Posted` possible values: - `All` - default value - `Past 24 hours` - `Last Week` - `Last Month` - `Last Year` `Content Type` possible values: - `All` - default value - `Stories` - `Clips` `Sort By` values: - `Relevance` - default value - `Like Count` - `Share Count` - `Date Posted` ## How to Use The `Search` feature can be enabled through CMS by setting the `enableSearch` value to `true`. Once enabled, the Search functionality will be available in both Clip and Story players without any additional changes required from the integrating app. Additional `Search` functionality is available through the `Storyteller` class: - `isSearchEnabled` - returns whether search functionality is enabled at the app level. - `openSearch` - opens the `Search`component from anywhere in the app. If Storyteller player is currently displayed, it will be dismissed before presenting the `Search` component. ## Customization Certain UI parts of the `Search` component can be customized. For more information, see [Themes](Themes.md). --- ## Source: DeepLinking.md # Deep Linking The Storyteller SDK makes it possible to handle the deep links. The implementing app should follow the official [Android guideline](https://developer.android.com/training/app-links/deep-linking). ## Showcase examples - [Compose — deep link handling (`DeeplinkHandler`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/DeeplinkHandler.kt#L40) - [XML — deep link handling (`MainActivity`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/MainActivity.kt#L44) In order to enable it separate intent filters need to be added to the implementing app's `AndroidManifest.xml`. ```xml ``` In the above, `[tenant_name]` is the lower case variant of your tenant's name. For example, if the tenant name is "MyTenant", then `[tenant_name]` should be "mytenant". Each link should be added as a separate intent-filter as in the example above. If Android system starting from Android 11 will fail to auto-verify any of the links which should be auto-verified it will disable the verification for all of them. This will result in the app not being able to handle any of the deep links in the Manifest. > Note: Please note that the last intent filter above for "[tenant_name]stories" does not have android:autoVerify="true" attribute. This is because the auto-verification is not supported for custom non https schemes. The deep link can be handled by using `Storyteller.openDeepLink`, which opens the link. This can be done automatically by using `openDeepLink` or manually using Story or Page ID (`openStory` and `openPage` respectively). Example 1: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) val data: Uri = intent?.data ?: return val url = data.toString() if (Storyteller.isStorytellerDeepLink(url)) { Storyteller.openDeepLink(this, url) { error -> // Handle error (content not found, invalid deep link, etc.) } } } ``` > Note: Starting from Android 6.0+, a Digital Asset Links file is required to properly handle `https` deep links. It can be generated inside the Storyteller CMS and > it requires you to add your app's package name and SHA-256 fingerprint of certificate the app was signed with. For more information on the Digital Asset Links file, see the [Android developer documentation](https://developer.android.com/studio/write/app-link-indexing#associatesite) To generate the Digital Asset Links file in the Storyteller CMS: 1. Navigate to the 'Apps' section on the left-hand side 2. Click on the 'New App' button 3. Fill out the 'Package Name' and 'SHA 256 Cert Fingerprint' - Package Name - this is the application ID declared in the app's `build.gradle` file. This value should be the same as the "App Store Identifier" entered above. E.g. `com.example.myapp.` - SHA 256 Cert Fingerprint - this is the SHA256 fingerprint of your app's signing certificate. You can use the following command to generate the fingerprint via the Java keytool: `keytool -list -v -keystore my-release-key.keystore`. Or, if using the Google Play Store Signing, the SHA256 signature can be downloaded using the Google Play Console: + Login to the Play Console + Select the app you want to configure + Go to Setup → App integrity + Copy the SHA-256 value under "App signing certificate" + This value should be entered into your Storyteller App's form without the "SHA-256: " prefix. E.g.`15:8D:E9:83:C5:73...` After saving your app, the Digital Asset Links file for your tenant can be viewed at the following URLs: `https://yourtenantname.ope.nstori.es/assetlinks.json` and `https://yourtenantname.ope.nstori.es/.well-known/assetlinks.json` **Note: the file may take up to 5 minutes to appear after saving your App.** ## Deep Link Handling ### Open Deep Link The `openDeepLink` function in the Storyteller SDK handles different types of deep links. ```kotlin fun openDeepLink(activity: Activity, url: String, onError: (StorytellerError) -> Unit = {}) ``` This call makes Storyteller open the provided deep link (showing the requested content). Use `Storyteller.isStorytellerDeepLink(url)` to check whether a URL is a Storyteller deep link before calling this method. Parameters: - `activity` - Activity. - `url` - this is the Deep Link URL. - `onError` - this is called when there is any issue with opening the deep link (e.g. the requested content is no longer available) #### Story Category - For story category deep links, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/category/` or `/go/category/` - The link includes a category identifier following the `/category/` part of the path - The category ID is extracted from the deep link and the `openCategory` function is called with the extracted category ID - eg: `"https://[tenantname].shar.estori.es/go/category/123456"` or `"https://[tenantname].shar.estori.es/open/category/123456"` #### Clip collection - For clip collection deep links, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/clip`, `/go/clip`, `/open/clips`, or `/go/clips` - The link includes a unique identifier (UUID) in the path - The link must include a `collectionId` query parameter - An optional `categoryId` query parameter can be included to specify a category to open in the Clip Collection as an initial category (`initialCategory` of the openCollection method) - The information extracted will be used to call `openCollection()` - eg: `"https://[tenantname].shar.estori.es/open/clip/((clipId))?collectionId=abcd1234&categoryId=123456"` #### Story Deep link - For story deep links, the openDeepLink method identifies and processes links based on the following structure: - The link is confirmed to be a URI object - It extracts storyId or pageId from the deep link via segment positions. One must be present - It proceeds to call `openPage()` or `openStory()` with either the storyId or pageId, - eg openStory: `https://[tenantname].shar.estori.es/go/story/((storyId))` or openPage: `https://[tenantname].shar.estori.es/go/page/((pageId))` #### Sheet - For sheet deep link, the openDeepLink method identifies and processes links based on the following structure: - The link contains either `/open/sheet/` or `/go/sheet/` - It extracts `sheetId` from the deep link via segment positions. One must be present - eg: `"https://[tenantname].shar.estori.es/go/sheet/123456"` or `"https://[tenantname].ope.nstori.es/open/sheet/123456"` ## Manual Deep Link Handling It is generally recommended to author your own deep link and extract the required data. This is to not upset the current state and deep link integration of your app and to allow for more control over the deep link handling then use `openStory()`/`openPage()`/`openCollection()`/`openCategory()`/`openSheet()` to pass the relevant data to the SDK, see the [Open Player](OpenPlayer.md) (or [AdditionalMethods](AdditionalMethods.md)) documentation for more information. ### isStorytellerDeepLink `isStorytellerDeepLink(String)` checks if the string is a valid Storyteller deep link. ```kotlin Storyteller.isStorytellerDeepLink("stories://open/9329bed2-2311-69b8-cbcf-39fcf8d8af21/f6445df7-bd79-71de-cdfb-39fd071568a1") ``` --- ## Source: NavigatingToApp.md # Navigating to App A `StorytellerDelegate` has method for managing in app navigation. It can be used for custom handling of `deeplink` URLs which can configured per page in CMS. This method will be called when user presses on the action button with the `deeplink` navigation type. ## Showcase examples - [Compose — `userNavigatedToApp` (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L117) - [XML — `userNavigatedToApp` (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L82) ## How to Use To use global `userNavigatedToApp`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object. `userNavigatedToApp(url: String)` is the method responsible for handling these `deeplink` URLs. ## Navigation to the Integrating App The callback `userNavigatedToApp` is called when a user taps on an action button on a page which has its link type set to `deeplink`. In this case, your app will be passed a URL which has been entered in the Storyteller CMS and your app is then responsible for parsing this URL and following it to the correct location within your app. Example: ```kotlin override fun userNavigatedToApp(url: String) { // parse the url and navigate to the destination } ``` --- ## Source: Themes.md # Custom Themes The appearance of the SDK can be customized by setting the `Storyteller.theme` global property or `StorytellerListView.theme` view property. This requires a `UiTheme` configuration object. > Note: global theme should be set before any of the Storyteller list views are inflated. > Note: this property will be used as fallback theming style used to render Story items in lists and activities launched from list. > See [StorytellerLists](StorytellerLists.md) or [StorytellerListViews](StorytellerListViews.md) (legacy). ## Showcase examples - [Compose — theme building (`GetConfigurationUseCase`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/domain/GetConfigurationUseCase.kt#L60) - [XML — theme selection (`GetHomeScreenUseCase`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/domain/GetHomeScreenUseCase.kt#L21) UiTheme is a data structure containing tree of the parameters. While it's possible to manually declare a whole structure of this data object, for convenience it's recommended to use UiTheme DSL for that purpose. ## Defining a Theme A `Theme` contains various settings which change visual aspects of the Storyteller SDK. All properties are optional. In general, it should be possible to obtain a custom look by only using the `colors` properties - however, if you require more fine-grained control, that is also available. ### Colors The `colors` property on theme is used to establish a set of base colors for the SDK to use. | Colors | Description | Default Value | Dark Value | | :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | :--------- | | `theme.colors.primary` | Default accent color used throughout the UI. Usually the primary brand color. Used for: Unread Indicators, loading progress bars and spinners on Android. | #1C62EB | | | `theme.colors.success` | Used to indicate correct answers in Quizzes. | #3BB327 | | | `theme.colors.alert` | Used to indicate wrong answers in Quizzes and for the 'Live' indicator on Live Stories. | #E21219 | | | `theme.colors.white.primary` | Used for Story names on rectangular tiles and in the player. Also used for all primary text in dark mode by default. | #ffffff | | | `theme.colors.white.secondary` | Used for secondary text in dark mode. | rgba({theme.colors.white.primary}, 85%) | | | `theme.colors.white.tertiary` | Used for tertiary text in dark mode, e.g. the time stamp in the player header. Also used for the selected Poll option border. | rgba({theme.colors.white.primary}, 70%) | | | `theme.colors.black.primary` | Used for primary text in light mode by default. | #1A1A1A | | | `theme.colors.black.secondary` | Used for secondary text in light mode. | rgba({theme.colors.black.primary}, 85%) | | | `theme.colors.black.tertiary` | Used for tertiary text and minor UI elements in light mode. | rgba({theme.colors.black.primary}, 70%) | | ### Font Use the `font` property to set a custom font for use throughout the UI. | Font | Description | Default Value | Dark Value | | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ | :--------- | | `theme.font` | Font to be used throughout the UI, defaults to the system font on each platform or inherits the container font on web. Font definition requires access to different weights to work properly. | System Font | | ### Primitives The `primitives` object contains base values which are used throughout the UI. | Primitives | Description | Default Value | Dark Value | | :------------------------------ | :------------------------------------------------------------------------------- | :------------ | :--------- | | `theme.primitives.cornerRadius` | Default corner radius used for Rectangular Tiles, Buttons and Poll/Quiz answers. | 8 dp | | ### Lists The `lists` customize properties of the various list types available from the SDK. | Lists | Description | Default Value | Dark Value | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------- | :--------------------------- | | `theme.lists.title` | The style of the title on Story or Clip tile. | 8 dp | | | `theme.lists.row.tileSpacing` | The space between each Story and Clip Tile in a row. | 8 dp | | | `theme.lists.row.startInset` | The space before the first tile in a row. | 12 dp | | | `theme.lists.row.endInset` | The space after the last tile in a row. | 12 dp | | | `theme.lists.grid.tileSpacing` | The space between Story and Clip Tiles in a grid, both vertically and horizontally. | 8 dp | | | `theme.lists.grid.columns` | Number of columns in the grid. | 2 dp | | | `theme.lists.grid.topInset` | The space at the top of the first row in a grid. | 0 dp | | | `theme.lists.grid.bottomInset` | The space at the bottom of the last row in a grid. | 0 dp | | | `theme.lists.backgroundColor` | Required for outline on Live chip and fade to the side of the row | {theme.colors.white.primary} | {theme.colors.black.primary} | | `theme.lists.animateTilesOnReorder` | This option allows you to enable animation in lists when updating items | true | true | | `theme.lists.enablePlayerOpen` | Controls whether the SDK opens the player when a tile is tapped. When set to `false`, the SDK will not open the player and the app must handle tile taps via `StorytellerDelegates.onTileTapped(tileType: StorytellerTileType)` | true | true | ### Gradient The `Gradient` data class allows for the creation of a color gradient, with options to customize both the colors and the positions at which the gradient starts and ends. | Property | Default Value | Data Type | Description | | --------------- | ------------- | ------------------ | -------------------------------------------------- | | `startColor` | `null` | `ColorInt` | The color where the gradient begins. | | `endColor` | `null` | `ColorInt` | The color where the gradient ends. | | `startPosition` | `null` | `GradientPosition` | The position indicating where the gradient starts. | | `endPosition` | `null` | `GradientPosition` | The position indicating where the gradient ends. | #### Enum: GradientPosition Defines positions for starting and ending points of the gradient. | Value | Data Type | Description | | ------------------------------- | --------- | ----------------------------------------- | | `GradientPosition.BottomLeft` | `enum` | Bottom left corner of the gradient area. | | `GradientPosition.BottomCenter` | `enum` | Bottom center edge of the gradient area. | | `GradientPosition.BottomRight` | `enum` | Bottom right corner of the gradient area. | | `GradientPosition.CenterLeft` | `enum` | Center left edge of the gradient area. | | `GradientPosition.CenterCenter` | `enum` | Center of the gradient area. | | `GradientPosition.CenterRight` | `enum` | Center right edge of the gradient area. | | `GradientPosition.TopLeft` | `enum` | Top left corner of the gradient area. | | `GradientPosition.TopCenter` | `enum` | Top center edge of the gradient area. | | `GradientPosition.TopRight` | `enum` | Top right corner of the gradient area. | ### Story and Clip Tiles The `tiles property` can be used to customize the appearance of the Story and Clip Tiles. | Story and Clip Tiles | Description | Default Value | Dark Value | | :-------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------ | :---------------------------- | | `theme.tiles.chip.textSize` | Text size for the New Indicator and Live Indicator. | 11 sp | | | `theme.tiles.chip.borderColor` | Border color for the New Indicator and Live Indicator. | null | | | `theme.tiles.chip.show` | Show or hide the New/Live chip indicator on tiles. | true | | | `theme.tiles.title.textSize` | Size of the Story Title on a Tile. | 11 sp | | | `theme.tiles.title.lineHeight` | Line height of the Story Title on a Tile. | 13 sp | | | `theme.tiles.title.alignment` | Alignment of the text in a tile, can be Gravity.START/CENTER/END | center | | | `theme.tiles.circularTile.title.unreadTextColor` | Text color of Circular Story and Clip Tile Titles in unread state | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.tiles.circularTile.title.readTextColor` | Text color of Circular Story and Clip Tile Titles in read state | {theme.colors.black.tertiary} | {theme.colors.white.tertiary} | | `theme.tiles.circularTile.unreadIndicatorColor` | The color of the ring around Circular Story and Clip Tiles. | {theme.colors.primary} | | | `theme.tiles.circularTile.readIndicatorColor` | The color of the ring around Circular Story and Clip Tiles in read state | #C5C5C5 (taken from app layout) | | | `theme.tiles.circularTile.unreadIndicatorBorderColor` | The color of the border of the ring around Circular Story and Clip Tiles | null | | | `theme.tiles.circularTile.readIndicatorBorderColor` | The color of the border of the ring around Circular Story and Clip Tiles in read state | null | | | `theme.tiles.circularTile.unreadBorderWidth` | Width of Circular Story and Clip Tile ring border in unread state | 2dp | | | `theme.tiles.circularTile.readBorderWidth` | Width of Circular Story and Clip Tile ring border in read state | 2dp | | | `theme.tiles.circularTile.unreadIndicatorGradient` | The gradient of the ring around a circular tile when the story or the clip is unread. If set, overrides `circularTile.unreadIndicatorColor`. | null | | | `theme.tiles.circularTile.liveChip.unreadImage` | Image resource to be used in place of default unread Live Indicator. If set, overrides `theme.tiles.circularTile.liveChip.unreadBackgroundGradient`. | null | | | `theme.tiles.circularTile.liveChip.unreadBackgroundGradient` | The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides `circularTile.liveChip.unreadBackgroundColor`. | null | | | `theme.tiles.circularTile.liveChip.unreadBackgroundColor` | Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.alert} | | | `theme.tiles.circularTile.liveChip.unreadBorderColor` | Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | null | | | `theme.tiles.circularTile.liveChip.unreadTextColor` | Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.white.primary} | | | `theme.tiles.circularTile.liveChip.readImage` | Image resource to be used in place of default read Live Indicator. | null | | | `theme.tiles.circularTile.liveChip.readBackgroundColor` | Background color of the Live Indicator when all pages have been read or the clip has been viewed. | {theme.colors.black.tertiary} | | | `theme.tiles.circularTile.liveChip.readBorderColor` | Border color of the Live Indicator when all pages have been read or the clip has been viewed. | null | | | `theme.tiles.circularTile.liveChip.readTextColor` | Text color of the Live Indicator when all story pages have been read or the clip has been viewed. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.title.textColor` | Text color of the Story Title in Rectangular Tiles. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.chip.alignment` | Alignment of the New Indicator and Live Indicator in Rectangular Tiles, can be start, center or end. | end | | | `theme.tiles.rectangularTile.padding` | Internal padding for Rectangular Story and Clip Tiles, creates space between Story Name or New Indicator and tile edge. | 8 dp | | | `theme.tiles.rectangularTile.unreadIndicator.image` | Image resource to be used in place of default New Indicator on Rectangular Tiles. | null | | | `theme.tiles.rectangularTile.unreadIndicator.backgroundColor` | Background color of the New Indicator. | {theme.colors.primary} | | | `theme.tiles.rectangularTile.unreadIndicator.borderColor` | Border color of the New Indicator. | null | | | `theme.tiles.rectangularTile.unreadIndicator.textColor` | The text color of the unread indicator for a rectangular tile. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.unreadIndicator.textSize` | Text size for the New Indicator. | 11 sp | | | `theme.tiles.rectangularTile.liveChip.unreadImage` | Image resource to be used in place of default unread Live Indicator. If set, overrides `theme.tiles.circularTile.liveChip.unreadBackgroundGradient`. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBackgroundGradient` | The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides `circularTile.liveChip.unreadBackgroundColor`. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBackgroundColor` | Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.alert} | | | `theme.tiles.rectangularTile.liveChip.unreadTextColor` | Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | {theme.colors.white.primary} | | | `theme.tiles.rectangularTile.liveChip.readImage` | Image resource to be used in place of default read Live Indicator. | null | | | `theme.tiles.rectangularTile.liveChip.readBackgroundColor` | Background color of the Live Indicator when all pages have been read or the clip has been viewed. | {theme.colors.black.tertiary} | | | `theme.tiles.rectangularTile.liveChip.readBorderColor` | Border color of the Live Indicator when all pages have been read or the clip has been viewed. | null | | | `theme.tiles.rectangularTile.liveChip.unreadBorderColor` | Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed. | null | | | `theme.tiles.rectangularTile.liveChip.readTextColor` | Text color of the Live Indicator when all story pages have been read or the clip has been viewed. | {theme.colors.white.primary} | | ### Player The `player` property is used to customize properties relating to the Stories and Clips Player. | Player | Description | Default Value | Dark Value | | :----------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | :--------- | | `theme.player.showStoryIcon` | Shows the circular Story icon before the Story Name in the Player. | FALSE | | | `theme.player.showTimestamp` | Shows a timestamp after the Story Name in the Player, eg "2h" to show the Story was published 2 hours ago. | TRUE | | | `theme.player.showShareButton` | Shows the Share button in the Player, applies to all Page types and Engagement Units. Setting to FALSE entirely disables sharing in Storyteller. | TRUE | | | `theme.player.liveChip.image` | Image used in place of Live Chip before Live Story or Clip Titles. If set, it overrides `liveChip.backgroundGradient` | null | | | `theme.player.liveChip.backgroundGradient` | Background gradient of the badge for Live Story or Clip. If set, it overrides `liveChip.backgroundColor` | null | | | `theme.player.liveChip.backgroundColor` | Background color of the Live chip Story or Clip | {theme.colors.alert} | | | `theme.player.liveChip.textColor` | Text color used for badge label for Live Story or Clip | {theme.colors.white.primary} | | | `theme.player.liveChip.borderColor` | Color used for border of badge label for Live Story or Clip | null | | | `theme.player.icons.share` | Share button image to be used in place of default share icon. | default icon | | | `theme.player.icons.refresh` | Refresh button image to be used in place of default refresh icon. | default icon | | | `theme.player.icons.close` | Close button image to be used in place of default close icon. | default icon | | | `theme.player.icons.back` | Back button image to be used in place of default Clips back icon. | default icon | | | `theme.player.icons.like.initial` | Initial like button image to be used in place of default initial like icon. | default icon | | | `theme.player.icons.like.liked` | Liked button image to be used in place of default liked icon. | default icon | | | `theme.player.showLikeButton` | Shows the Like button on Clips. | TRUE | | Example: ```kotlin theme.light.player { icons { back = drawableRes(R.drawable.custom_clips_back) } } ``` ### Buttons The `buttons` property applies customizations to buttons which appear throughout the UI. | Buttons | Description | Default Value | Dark Value | | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------ | :--------- | | `theme.buttons.backgroundColor` | Background color of buttons including: share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. | {theme.colors.white.primary} | | | `theme.buttons.textColor` | Text color of buttons including: share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. | {theme.colors.black.primary} | | | `theme.buttons.textCase` | Sets the text case for the button on the Instructions Screen, share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state (TextCase.UPPER/DEFAULT/LOWER). | default | | | `theme.buttons.cornerRadius` | Sets the corner radius for buttons including: Instructions Screen button, share buttons at the end of Quizzes, primary action buttons in Clips, and action buttons in the Following empty state. Any value greater than half the height of the button will create a pill shape. | {theme.primitives.cornerRadius} | | ### Instructions Use the `instructions` property to customize the appearance of the instructions screen. | Instructions | Description | Default Value | Dark Value | | :------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | :----------------------------- | | `theme.instructions.show` | Show the Instructions Screen the first time a user opens Storyteller. Set to FALSE to entirely hide the Instructions screen. | TRUE | | | `theme.instructions.headingColor` | Heading color of the text used on the Instructions Screen. | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.instructions.headingTextCase` | Sets the text case for the heading on the Instructions Screen. (TextCase.UPPER/DEFAULT/LOWER). | TextCaseTheme.DEFAULT | | | `theme.instructions.headingFont` | Font to be used on the Instuctions Screen for headers, defaults to the system font | System font | | | `theme.instructions.subHeadingColor` | Subheading color of the text used on the Instructions Screen. | {theme.colors.black.secondary} | {theme.colors.white.secondary} | | `theme.instructions.backgroundColor` | Background color of the Instructions Screen. | {theme.colors.white.primary} | {theme.colors.black.primary} | | `theme.instructions.icons` | A set of icons used for each instruction on the Instructions Screen | default set of icons | | | `theme.instructions.button.backgroundColor` | Background color of the button used on the Instructions Screen. | {theme.colors.black.primary} | {theme.colors.white.primary} | | `theme.instructions.button.textColor` | Text color of the button used on the Instructions Screen. | {theme.colors.white.primary} | {theme.colors.black.primary} | ### Engagement Units The `engagementUnits` property can be used to customize properties relating to Polls and Quizzes. | Engagement Units | Description | Default Value | Dark Value | | :----------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | :--------- | | `theme.engagementUnits.poll.answerTextColor` | Answer text color used in Poll Answers. | {theme.colors.black.primary} | | | `theme.engagementUnits.poll.percentBarColor` | Background color of the Percentage Bar in Poll Answers. | #CDD0DC | | | `theme.engagementUnits.poll.selectedAnswerBorderColor` | Border color added to the selected Poll Answer. Inherits `colors.primary` | {theme.colors.primary} | | | `theme.engagementUnits.poll.answeredMessageTextColor` | Color of the vote count or "Thanks for Voting!" message shown to users. | {theme.colors.white.primary} | | | `theme.engagementUnits.poll.selectedAnswerBorderImage` | Border image used for the selected Poll Answer. Overwrites selectedAnswerBorderColor and can be used to create a shimmer animation as the border image is rotated in when an answer is selected. | null | | | `theme.engagementUnits.poll.showPercentBarBackground` | Adds a striped background under the percentage bar in Poll Answers. | FALSE | | | `theme.engagementUnits.triviaQuiz.correctColor` | Color used for correct answers in Trivia Quizzes. | {theme.colors.success} | | | `theme.engagementUnits.triviaQuiz.incorrectColor` | Color used for incorrect answers in Trivia Quizzes. | {theme.colors.alert} | | ### Search The `search` property applies customizations to the `Search` component. | Title | Description | Default Value | Dark Value | | ----------------------- | ------------------------------------------------- | ------------------- | ------------------- | | `theme.search.heading` | Styling of the `Filters` title in the Filter View | `theme.lists.title` | `theme.lists.title` | | `theme.search.backIcon` | Image to be used as a back icon in the Search UI | default icon | | ### Home | Title | Description | Default Value | Dark Value | | ---------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ---------- | | `theme.home.headerTitle.font` | Font to be used throughout the UI, defaults to the system font | System font | | | `theme.home.headerTitle.textSize` | Size of the Home Header Title | 22sp | | | `theme.home.headerTitle.lineHeight` | Line height of the Home Header Title | 25sp | | | `theme.home.headerTitle.textCase` | Sets the text case for the Home Header Title. (TextCase.UPPER/DEFAULT/LOWER). | TextCaseTheme.DEFAULT | | | `theme.home.headerTitle.textColor` | Text color of the Home Title | colors.black.primary | | | `theme.home.circularTitle.textSize` | The size of the title for Circular tiles | lists.title | | | `theme.home.circularTitle.lineHeight` | The line height of the title for Circular tiles | lists.title | | | `theme.home.gridTitle.textSize` | The size of the title for tiles in Grid | 16sp | | | `theme.home.gridTitle.lineHeight` | The line height of the title for Grid tiles | 22sp | | | `theme.home.singletonTitle.textSize` | The size of the title for Singleton tile | 22sp | | | `theme.home.singletonTitle.lineHeight` | The line height of the title for Singleton tiles | 28sp | | | `theme.home.rectangularTitle.smallTitle.textSize` | The size of the title for small Rectangular row tiles | lists.title | | | `theme.home.rectangularTitle.smallTitle.lineHeight` | The title line height for the small Rectangular row tiles | lists.title | | | `theme.home.rectangularTitle.mediumTitle.textSize` | The size of the title for medium Rectangular row tiles | 16sp | | | `theme.home.rectangularTitle.mediumTitle.lineHeight` | The title line height for the medium Rectangular row tiles | 22sp | | | `theme.home.rectangularTitle.largeTitle.textSize` | The size of the title for large Rectangular row tiles | 18sp | | | `theme.home.rectangularTitle.largeTitle.lineHeight` | The title line height for the large Rectangular row tiles | 24sp | | ### Theme Builder Initialization Theme builder is initialized by the `buildTheme` method lambda passed after the method acts in the builder scope. ```kotlin import com.storyteller.domain.theme.builders.buildTheme import com.storyteller.domain.theme.builders.ofHexCode Storyteller.theme = buildTheme { light.colors.primary = ofHexCode("#FF00FF") } ``` ### Accessing Properties in the Builder Scope There are 2 equivalent ways of accessing builder properties: - By scopes ```kotlin buildTheme { light { instructions { button { backgroundColor = ofHexCode("#00FF00") } } } } ``` - By properties ```kotlin buildTheme { light.instructions.button.backgroundColor = ofHexCode("#00FF00") } ``` Both approaches produce the same effect. ### Light and Dark Builder Variants In the builder context two variants are present: - light - dark Although they have an identical structure, they are build with the different set of fallbacks. For instance, default `theme.tiles.circularTile.title.unreadTextColor` will fallback to the default value of `theme.colors.black.primary` in the light mode or `theme.colors.white.primary` in the dark mode. The selection of active themes will be done using current phone UI mode and `StorytellerListView.uiStyle` property value. See [StorytellerListViews](StorytellerListViews.md) for more details. For coding convenience, if you do not intent use light and dark mode and relay on default fallback you can use `from` inline method to copy already set values from one theme to the other. ```kotlin buildTheme { light { colors { primary = ofHexCode("#FF00FF") success = ofHexCode("#00FF00") } } dark from light } ``` The above code will set all properties of dark to the current state of the light builder. This method is useful to avoid lengthy typing - a common parameter can be assigned one and copied to the other variant. ### Setting Properties of Particular Type #### Colors Color properties are expected to be Android @ColorInt. They can be initialized with anything that return such type e.g they can be resolved color from resources, Color.argb() Color.BLUE and so on. For convenience, `ofHexColor(string)` method is provided - it accepts 6 or 8 hex digits prefixed by the `#` ```kotlin val red = ofHexColor("#FF0000") val semiTransparentRed = ofHexColor("#55FF0000") ``` > Note: when using resources colors mind that they are resolved at the moment of building theme, **NOT** at the moment of accessing. #### Drawables For the properties accepting StorytellerDrawable type. You can use `drawableRes` helper method to set a drawable resource. ```kotlin val myTheme = buildTheme { light.engagementUnits.poll.selectedAnswerBorderImage = drawableRes(R.drawable.gradient_border) } ``` #### Fonts To support multiple weights for fonts, a font family xml resource is required. The SDK will automatically select a font for the appropriate weight when needed. - Creating a font family resource `custom_font.xml` ```xml ``` - Setting the font family as custom font property `theme.font` using the helper method `fontRes` ```kotlin font = fontRes(R.font.custom_font) ``` ## Example ```kotlin val storyRowView = StoryRowView(context) storyRowView.theme = buildTheme { light { colors { primary = ofHexCode("#FF0000") success = ofHexCode("#00FF00") alert = ofHexCode("#C50511") } font = fontRes(R.font.custom_font) lists { row { tileSpacing = 8 startInset = 12 endInset = 12 } grid { tileSpacing = 8 columns = 2 } enablePlayerOpen = false // Handle tile taps manually via StorytellerDelegates } tiles { title { textSize = 11 lineHeight = 13 alignment = Gravity.START } circularTile { unreadIndicatorGradient = UiTheme.Theme.Gradient( startColor = getColor(R.color.gradient_start), endColor = getColor(R.color.gradient_end), startPosition = UiTheme.Theme.Gradient.GradientPosition.CenterLeft, endPosition = UiTheme.Theme.Gradient.GradientPosition.CenterRight, ) liveChip { unreadBackgroundGradient = UiTheme.Theme.Gradient( startColor = getColor(R.color.gradient_start), endColor = getColor(R.color.gradient_end), startPosition = UiTheme.Theme.Gradient.GradientPosition.CenterLeft, endPosition = UiTheme.Theme.Gradient.GradientPosition.CenterRight, ) readTextColor = getColor(R.color.read_live_text) unreadTextColor = getColor(R.color.unread_live_text) } } rectangularTile { padding = 8 unreadIndicator.alignment = Gravity.END unreadIndicator.textColor = ofHexCode("#000FF") unreadIndicator.textSize = 11 liveChip { readTextColor = getColor(R.color.read_live_text) unreadTextColor = getColor(R.color.unread_live_text) } } } buttons.cornerRadius = 24 buttons.textCase = TextCase.UPPER instructions { icons { forward = drawableRes(R.drawable.ic_forward_light) pause = drawableRes(R.drawable.ic_pause_light) back = drawableRes(R.drawable.ic_back_light) move = drawableRes(R.drawable.ic_move_light) } } home { title { font = fontRes(R.font.font) textCase = TextCaseTheme.LOWER textSize = 28 lineHeight = 28 textColor = getColor(R.color.rams_storyteller_primary) } } } dark from light } ``` --- ## Source: StorytellerDelegates.md # Implementing Storyteller Delegate Callbacks A `StorytellerDelegate` has methods for managing `Storyteller` events. It is used as global object for handling all events when opening a Story Player with and without the `StorytellerStoriesRowView` or `StorytellerStoriesGridView`. Please see the dedicated `StorytellerListViewDelegate` below for handling events related to rows and grids. ## Showcase examples - [Compose — global delegate (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L32) - [Compose — list delegate example (`PageItemStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/storyteller/PageItemStorytellerDelegate.kt#L14) - [XML — global delegate (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L28) - [XML — list delegate example (`StorytellerViewDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/dashboard/adapter/StorytellerViewDelegate.kt#L13) ## StorytellerDelegate To use global `StorytellerDelegate`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object. Example: ```kotlin Storyteller.storytellerDelegate = myCustomStorytellerDelegate ``` ### onUserActivityOccured The `onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData)` method is is called when an analytics event is triggered. See the dedicated [Analytics](Analytics.md) page for more information on analytic events. ### getAd The `getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit)` method is called when the tenant is configured to request ads from the containing app and the SDK requires ad data from the containing app. For more information on how to supply ads to the Storyteller SDK, see the dedicated [Ads](Ads.md) page. ### userNavigatedToApp The `userNavigatedToApp(url: String)` method is called when a user taps on an action button on a Page which should direct the user to a specific place within the integrating app. More information on `In App` links [Navigating to App](NavigatingToApp.md). For more information on deep linking, see the dedicated [Deep Linking](DeepLinking.md) page. ## Analytics The callback `onUserActivityOccurred` provides analytics events and corresponding data triggered internally by the SDK. This information can be used in your app. The following parameters are passed to the callback method: - `type` - type of event that occurred, as a `StorytellerUserActivity.EventType` enum - `data` - an object containing data about the event which occurred Example: ```kotlin ... override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) { if (type == StorytellerUserActivity.EventType.OPENED_STORY) { // Retrieve the story id value val openStoryId = data.storyId // Retrieve the story title value val openStoryTitle = data.storyTitle // Report retrieved values from your app } } ``` For a detailed discussion of all the relevant events and properties please see the dedicated [Analytics](Analytics.md) page. ## Client Ads By implementing `getAd`, you can provide custom ad data for the SDK to render, this is only applicable when the ad configuration is set to `Integrating App` in the CMS. Ad data can be obtained asynchronously, and should be provided using the `onComplete` closure parameter. Example: ```kotlin ... override fun getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit) { // Action to get some ads val ad = getMyAd() // Convert the ad to a StorytellerAd val storytellerAd = convertToStorytellrAd(ad) // Provide the ad to the SDK onComplete(ad) } ``` For a detailed discussion of all the relevant considerations, please see the dedicated [Ads](Ads.md) page. ### configureWebView This method allows you to configure the WebView with custom settings or actions when the Storyteller SDK, is to display a WebView. It takes three parameters: the WebView to configure, an optional URL string, and an optional favicon Bitmap. This method is called from the `onPageStarted` method of a custom `WebViewClient` when a new page starts loading in the WebView. #### Parameters - `view: WebView` - The WebView instance to be configured. - `url: String?` - (Optional) The URL string associated with the WebView page that is being loaded. - `favicon: Bitmap?` - (Optional) The favicon Bitmap to be displayed for the WebView page. #### Example Usage To intercept and customize WebViews created by the Storyteller SDK, you can implement your own version of the `configureWebView` method. Here is an example of how to do this: ```kotlin override fun configureWebView( view: WebView, url: String?, favicon: Bitmap? ) { // Your custom configuration code goes here // The following line shows a simple alert in the WebView with the message "test webview javascript". view.evaluateJavascript("javascript: alert('test webview javascript');", null) } ``` ## Followable Categories The method `fun categoryFollowActionTaken(category: Category, isFollowing: Boolean)` is invoked when a user adds or removes a category from within SDK's UI. The callback method receives the following parameters: - `category` - An object representing the clip category - `isFollowing` - A boolean value indicating whether the user is following or unfollowing the specified category ### customScreenForCategory The `fun customScreenForCategory(): @Composable (StorytellerFollowableCategoryCustomScreen) -> Unit` method is called when a user navigates to a followable category screen. The callback receives a `StorytellerFollowableCategoryCustomScreen` object. with the following parameters: - `pendingModifier` - a `Modifier` object that is used to apply custom styling to the screen, this must be applied to the root composable of your custom screen - `category` - a `Category` object that is the category that the user is navigating to - `onBackClicked` - a `() -> Unit` a callback that should be called when the user clicks the back button from the custom screen, this is to let Storyteller know that the user has navigated back to the previous screen. If this is not called, the user will be stuck on the custom screen and unable to navigate back to the previous screen. Example: ```kotlin override fun customScreenForCategory(): @Composable() ((StorytellerFollowableCategoryCustomScreen) -> Unit) { return { (pendingModifier, category, onBackClicked) -> Scaffold( modifier = pendingModifier, topBar = { CenterAlignedTopAppBar( title = { Text("${category.displayTitle}") }, navigationIcon = { IconButton(onClick = onBackClicked) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back" ) } } ) } ) { paddingValues -> Surface( modifier = Modifier.padding(paddingValues) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center ) { Text("Custom layout for ${category.displayTitle}") } } } } } ``` Alongside the `customScreenForCategory` callback, the `Storyteller` object also has a `useCustomScreenForCategory` property that is a `(Category) -> Boolean` callback. This is to allow the developer to conditionally show the custom screen for a specific category or default to the original implementation of Followable Categories by Storyteller. The callback must return a `Boolean` value, which determines whether the custom screen should be shown for the given category. Example: ```kotlin Storyteller.useCustomScreenForCategory = { if (it.type == "custom") { // return true if you want to show the custom followable category screen true } else { // return false if you want to show the default followable category screen false } } ``` The `customScreenForCategory` will only be called if the `useCustomScreenForCategory` callback returns `true` for the given category. Otherwise, if it's not set or returns `false`, the default implementation of the Followable Categories by Storyteller will be used. ### bottomSheetScreen The `fun bottomSheetScreen(urlProvider: () -> String, onDismiss: () -> Unit): @Composable () -> Unit` method is called when the deep link includes the query parameter `shouldUseModal=true`. - `urlProvider` A callback that provides the URL to be loaded in the WebView or used for other purposes. - `onDismiss` A callback invoked when the user dismisses the bottom sheet. Example: ```kotlin override fun bottomSheetScreen( urlProvider: () -> String, onDismiss: () -> Unit ): @Composable (() -> Unit) { return { DemoBottomSheet(urlProvider, onDismiss) } } ``` ## StorytellerListViewDelegate A `StorytellerListViewDelegate` has methods for managing `StorytellerStoriesRowView` and `StorytellerStoriesGridView` events. ### How to Use To use `StorytellerListViewDelegate`, implement the `StorytellerListViewDelegate` interface and override the required methods: #### onDataLoadStarted The `onDataLoadStarted()` method is called when the network request to load data for all Stories has started. #### onDataLoadComplete The `onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int)` method is called when the data loading network request is complete. | Property | Description | | ----------- | ------------------------------------------------------- | | `success` | This confirms whether or not the request was successful | | `error` | The HTTP error if the request was not successful | | `dataCount` | The number of Stories loaded | #### onTileTapped The `onTileTapped(tileType: StorytellerTileType)` method is called when a user taps on a tile inside a row or grid. This callback is executed before the player Activity is opened. | Property | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------- | | `tileType` | Sealed class describing the tapped tile (Story or Clip). | | | Story: `storyId`, `categories`. Clip: `clipId`, `collectionId`, `categories` (`List`). | Example: ```kotlin override fun onTileTapped(tileType: StorytellerTileType) { when (tileType) { is StorytellerTileType.Story -> { // Handle story tile tap val storyId = tileType.id val categories = tileType.categories } is StorytellerTileType.Clip -> { // Handle clip tile tap val clipId = tileType.id val collectionId = tileType.collectionId val categories = tileType.categories } } } ``` > Note: When `theme.lists.enablePlayerOpen` is set to `false`, this callback becomes the primary way to handle tile interactions, as the SDK will not automatically open the player. #### onPlayerDismissed The `onPlayerDismissed()` method is called when user closes Storyteller Player. Example: ```kotlin val StorytellerStoriesRowView = StorytellerStoriesRowView() StorytellerStoriesRowView.delegate = myDelegate ``` #### Error Handling By using the callback function `onDataLoadComplete` and the data it provides, you can handle the current state of the `StorytellerStoriesRowView` appropriately in your app. > Note: `dataCount` is the total number of Stories in the existing `StorytellerStoriesRowView` at any given time Example: ```kotlin ... override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (success) { // stories data has been loaded successfully // dataCount is the current total number of stories, including newly added/removed data } else if (error != null) { // an error has occurred, unwrap the error value for more information // dataCount is the total number of stories before loading began } } ``` Another example: ```kotlin ... override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) { if (error != null && dataCount == 0) { // stories have failed to load with error and there is no data to show // you may wish to hide the `StorytellerStoriesRowView` instance here StorytellerStoriesRowView.visibility = View.GONE } } ``` --- ## Source: AdditionalMethods.md # Additional Methods This page covers additional helper methods and properties. ## Showcase examples - [Compose — `Storyteller.isSearchEnabled` (`StorytellerTopAppBar`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/main/StorytellerTopAppBar.kt#L87) - [Compose — `Storyteller.dismissPlayer` before navigation (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L121) - [XML — `Storyteller.dismissPlayer` before navigation (`ShowcaseStorytellerDelegate`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ShowcaseStorytellerDelegate.kt#L86) ## Setting the global StorytellerDelegate Please see the dedicated [Storyteller Delegates](StorytellerDelegates.md) page. ```kotlin Storyteller.storytellerDelegate = myCustomStorytellerDelegate ``` ## Static Attributes ### isInitialized `isInitialized` is a boolean static property which is set to `true` if Storyteller was initialized successfully. ```kotlin val initialized = Storyteller.isInitialized ``` ### isSearchEnabled `isSearchEnabled` is a boolean static property which is set to `true` if the Search feature is enabled in the Storyteller configuration. ```kotlin val searchEnabled = Storyteller.isSearchEnabled ``` ### isPlayerVisible `isPlayerVisible` is a boolean property which is set to `true` when a Story or Clip player is opened, `false` when the Story or Clip player is dismissed. ```kotlin val isStoryPlayerVisible = Storyteller.isPlayerVisible ``` ### version The `version` is a static String property which holds the SDK version. ```kotlin val storytellerVersion = Storyteller.version ``` ### user Allows setting custom attributes for audience targeting. Please see [Working with Users](Users.md). ```kotlin Storyteller.user.setCustomAttribute("location", "New York") ``` ## Static Methods ### openSearch ```kotlin fun openSearch(activity: Activity) ``` This method opens the Search screen. Parameters: - `activity` - this is the Activity that will be used to launch the Storyteller Search activity ### openSheet ```kotlin fun openSheet(activity: AppCompatActivity, sheetId: String, onError: (StorytellerError) -> Unit) ``` This call opens a Sheet with a given ID. Parameters: - `activity` - this is the Activity that will be used to launch the Sheet - `sheetId` - this is a Sheet's ID. If it is blank, `onError` will be called - `onError` - this is called when there is any issue with opening a Sheet (e.g. the sheetId is a blank string) ### dismissPlayer `dismissPlayer(animated: Boolean = true, reason: String? = null, onCompletion: () -> Unit = {})`: force closes the currently open Story or Clip player. If no player is open when this is called, it has no effect. `animated`: the flag indicating if close animation should be triggered. Defaults to `true`. `reason`: the reason why the Story Page was force closed. This will be used to populate the `dismissedReason` parameter of the corresponding `onUserActivityOccurred` callback. If this is set to `null` the `onUserActivityOccurred` callback will not be triggered. `onCompletion`: a callback that will be called when the player is dismissed. This is useful when you want to perform an action after the player is dismissed. ```kotlin Storyteller.dismissPlayer(true, "reason") ``` ### resumePlayer `resumePlayer()`: resumes the currently open Story or Player Views. If no Player is open when this is called, it has no effect. ```kotlin Storyteller.resumePlayer() ``` ### getStoriesCount ```kotlin suspend fun getStoriesCount(categoryIds: List): Int ``` This method returns the count of stories available in the specified categories. It can be used to determine if a particular category or set of categories contains any Stories before rendering a UI component. Parameters: - `categoryIds` - a list of category IDs to check for stories Return value: - Returns the total number of stories available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty ### getClipsCount ```kotlin suspend fun getClipsCount(categoryIds: List): Int ``` This method returns the count of clips available in the specified categories. It can be used to determine if a particular category or set of categories contains any Clips before rendering a UI component. Parameters: - `categoryIds` - a list of category IDs to check for clips Return value: - Returns the total number of clips available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty ### preloadClips ```kotlin @MainThread fun preloadClips( collection: String, clipsIds: List = emptyList(), preloadVideos: Boolean = false ): StorytellerClipPreloadHandle? ``` Preloads a Clips collection and optional single Clip IDs into the in-memory cache. This fetches the first page of the collection and stores it in the Clips cache, then fetches each requested clip by ID so single‑clip requests can read from cache. The loaded clips are also used to warm the playcard image cache to avoid a blank flash on first open. **Important:** You must retain a strong reference to the returned `StorytellerClipPreloadHandle` while preloading is in progress. If the handle is garbage collected, preloading will stop. Parameters: - `collection` - The Clips collection ID to preload. - `clipsIds` - Optional list of Clip IDs to preload (useful for clips beyond page 0). - `preloadVideos` - Whether to preload clip video content in addition to playcards. Defaults to `false`. Return value: - Returns a `StorytellerClipPreloadHandle` that must be retained to keep preloading active. Call `cancel()` on the handle to stop preloading. Returns `null` if the SDK is not initialized or the collection is blank. Best practices: - Only preload IDs for clips that are likely to be viewed soon (e.g., visible list items) - Call `StorytellerClipPreloadHandle.cancel()` when the preloaded clips are no longer needed - Large lists may contend with playback bandwidth; limit to near-term items - Include any single clip IDs that are outside the first page of the collection Example usage: ```kotlin class MyClipListViewModel : ViewModel() { private var preloadHandle: StorytellerClipPreloadHandle? = null fun onClipsVisible(collectionId: String, clipIds: List) { // Cancel previous preloads preloadHandle?.cancel() // Start new preloads for the collection + visible clips preloadHandle = Storyteller.preloadClips(collectionId, clipIds, preloadVideos = false) } override fun onCleared() { super.onCleared() preloadHandle?.cancel() } } ``` --- ## Source: Analytics.md # Analytics ## Showcase examples - [Compose — map `onUserActivityOccurred` to Amplitude](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/amplitude/AmplitudeAnalyticsManager.kt#L19) - [XML — map `onUserActivityOccurred` to Amplitude](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/amplitude/AmplitudeAnalyticsManager.kt#L18) ## Event Types These are the various events which are triggered from within the SDK. Each event is a member of the `StorytellerUserActivity.EventType` enumeration. Please see the [StorytellerDelegates](StorytellerDelegates.md) page for details on how to bind to these events. In the below discussion, "completing" a Page refers to allowing the timer to expire - so this would correspond to watching all of an image or a Poll Page for the duration set for it (default 15s) or watching all of a video. ## Story Events ### Opened Story This event is recorded in the following scenarios: - When a user taps on a list item to open a Story - When a Story is loaded because the previous Story finished - When a Story is loaded because the user tapped to skip the last Page of the previous Story - When a user swipes left on a Story to go to the next Story - When a user swipes right on a Story to go to the previous Story - When a user is sent directly to a Story via a deep link Whenever an Opened Story event occurs, event data for `storyID`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyReadStatus`, `storyPlaybackMode`, `openedReason`, `currentCategory`, `collection`, `categories`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Opened Page This event is recorded as soon as a user sees content for a Page. Opened Page is one of the most important events to track as it is equal to a video start, one of the most important measures of engagement. By tracking this event, you can monitor valuable information about user engagement with your app. Whenever an Opened Page event occurs, event data for `pageID`, `pageIndex`, `pageType`, `storyPlaybackMode`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `contentLength`, `storyID`, `storyTitle`, `storyIndex`, `storyPageCount`, `currentCategory`, `collection`, `openedReason`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Story This event is recorded in the following scenarios: - When a user taps the close button to dismiss the Story - When a user swipes down to dismiss the Story - When a user taps back on their device UI to dismiss the Story (Android only) - When a user taps to skip the last Page of the final Story - this dismisses the Story and exits the Story View - When a user swipes left on the final Story to dismiss the Story - When a user swipes right on the first Story to dismiss the Story - When a user completes the final Page of the final Story and the Story view is dismissed Whenever a Dismissed Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType` `dismissedReason`, `durationViewed`, `currentCategory`, `collection`, `pagesViewedCount`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Skipped Story This event is recorded when: - a user swipes left to go to the next Story - a user skips the last Page of a Story to go to the next Story Whenever a Skipped Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Skipped Page This event is recorded when a user taps to go to the next Page before completing the current Page. Whenever a Skipped Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Story This event is recorded at the same time as `OpenedPage` for the final Page in a Story. Whenever a Completed Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Page This event is recorded when a user watches a Page to completion (i.e. the timer for that Page finishes). Whenever a Completed Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `contentLength`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Action Button Tapped This event is recorded when a user taps on an action button on a Page to open a link. Whenever a Action Button Tapped event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `collection`, `actionText`, `pageActionUrl`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Button Tapped This event is recorded when a user taps the share button on a Page. Whenever a Share Button Tapped event occurs, event data for `shareMethod`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Story This event is recorded when: - a user swipes right to go to the previous Story (unless this is the first Story - in which case DismissedStory is fired instead) - a user taps back on the first Page in a Story (and this is not the first Page in the first Story) Whenever a Previous Story event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Page This event is recorded when a user taps back to go to a previous Page in the Story. Whenever a Previous Page event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `currentCategory`, `collection`, `pageType`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Success This event is recorded when a user selects a sharing method from the system dialog. Note: this event is only available for Android API level 22 and above. Whenever a Share Success event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPageCount` `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `shareMethod`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `isMuted` and `context` is returned. > Note 1: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. > Note 2: sharing to clipboard does not trigger Share Success event, only when an app is selected for sharing the event will be fired. ### Voted Poll This event is recorded when a user votes in a poll. Whenever a Voted Poll event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageId`, `pageIndex`, `pageType`, `currentCategory`, `collection`, `pollAnswerId`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Trivia Quiz Question Answered This event is recorded when a user submits a Quiz answer. Whenever a Trivia Quiz Question Answered event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `triviaQuizId`, `triviaQuizTitle`, `triviaQuizQuestionId`, `currentCategory`, `collection`, `triviaQuizAnswerId`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Trivia Quiz Completed This event is recorded when a user completes a Quiz. Whenever a Trivia Quiz Completed event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `storyPlaybackMode`, `pageID`, `pageIndex`, `pageType`, `triviaQuizId`, `triviaQuizTitle`, `currentCategory`, `collection`, `triviaQuizScore`, `actionLinkId`, `captionsEnabled`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Muted Story This event is recorded when a user taps the mute button to mute audio while viewing a Story. Whenever a Muted Story event occurs, event data for `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyId`, `storyIndex`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `pageHasAction`, `pageActionType`, `pageId`, `pageIndex`, `pageTitle`, `pageType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyPlaybackMode`, `storyTitle`, `isMuted` and `context` is returned. ### Unmuted Story This event is recorded when a user taps the mute button to unmute audio while viewing a Story. Whenever an Unmuted Story event occurs, event data for `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyId`, `storyIndex`, `pageActionText`, `pageActionUrl`, `actionLinkId`, `pageHasAction`, `pageActionType`, `pageId`, `pageIndex`, `pageTitle`, `pageType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyPlaybackMode`, `storyTitle`, `isMuted` and `context` is returned. ## Clip Events ### Opened Clip This event is recorded when: - a user taps on a row or grid item to open a Clip - a user swipes up to the next Clip - a user swipes down to the previous Clip - a user is sent directly to a Clip via a call to `openCollection` - a user is sent directly to a Clip via a deep link - a user dismisses the last category (by pressing back) and returns to the top level collection Whenever a Opened Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `clipHasAction`, `clipActionType`, `openedReason`, `collection`, `categories`, `contentLength`, `clipFeedType`, `categoryDetails`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Clip This event is recorded when: - a user taps on the back button in the top-left to exit the Clips player (and is at the top of the stack of Clip Categories) - a user taps back on their device UI to navigate back (Android only) This event does not fire when the user is not at the top of the Clip Category stack. Whenever a Dismissed Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `dismissedReason`, `durationViewed`, `clipsViewed`, `collection`, `categories`, `clipFeedType`, `loopsViewed`, `captionsEnabled`, `actionLinkId`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Next Clip This event is recorded when a user swipes up to go to the next Clip. Whenever a Next Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Previous Clip This event is recorded when a user swipes down to go to the previous Clip. Whenever a Previous Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Completed Loop This event is recorded when a user completes a loop of a Clip. Whenever a Completed Loop event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `completionType`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired. ### Action Button Tapped This event is recorded when: - a user taps the primary action button at the bottom of a Clip - a user taps any secondary action buttons above the clip title - a user swipes left on a Clip to open the relevant action Whenever an Action Button Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `clipHasAction`, `clipActionType`, `clipActionText`, `collection`, `categories`, `clipFeedType`, `actionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `actionClass`, `actionIndex`, `tappedClipActionText`, `tappedClipActionUrl`, `tappedClipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Button Tapped This event is recorded when a user taps the share button on a Clip. Whenever a Share Button Tapped event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Share Success This event is recorded when a user selects a sharing method from the system dialog. Whenever a Share Success event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `shareMethod`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. ### Paused Clip This event is recorded when a user taps on the screen whilst a Clip is playing to pause the Clip. Tt does not fire when a Clip is paused automatically by sharing or following an action. Whenever a Paused Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired, as pausing/resuming live clips is not supported. ### Resumed Clip This event is recorded when a user taps a Clip that is paused to resume playback. It does not fire when a Clip is resumed automatically. Whenever a Resumed Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. > Note: For live clips this event never will be fired, as pausing/resuming live clips is not supported. ### Liked Clip This event is recorded when a user likes a Clip by tapping the like button when they do not currently like the Clip. Whenever a Liked Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Unliked Clip This event is recorded when a user unlikes a Clip by tapping the like button when they currently like the Clip. Whenever a Unliked Clip event occurs, event data for `clipId`, `clipTitle`, `collection`, `categories`, `clipFeedType`, `clipIndex`, `clipCollectionCount`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Finished Clip This event is recorded when: - Dismissed Clip is fired - Next Clip is fired - Previous Clip is fired - At the same time as OpenedCategory - (iOS specific triggers exist for loop completion, For You/Following switching) Whenever a Finished Clip event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `loopsViewed`, `durationViewed`, `collection`, `categories`, `clipFeedType`, `contentLength`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `completionType`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Opened Category This event is recorded when: - a user taps a category at the bottom of the Clips Player to open a new Category - a user opens a category by navigating back from another category This event is not fired when opening the top level of a collection - only when navigating to a specific category. Whenever a Opened Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Category This event is recorded when: - a user navigates back using the back button (or system back on Android) to the previous category. This event does not fire when dismissing the top level collection Whenever a Dismissed Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Follow Category This event is recorded when: - a user taps the plus button under the follow category button of the Clips Player - a user taps the follow button at the top right of the Followable Category screen and the category was not followed Whenever a Follow Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Unfollow Category This event is recorded when: - a user taps the checkmark button under the follow category button of the Clips Player - a user taps the follow button at the top right of the Followable Category screen and the category was followed Whenever a Unfollow Category event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Tapped This event is recorded when: - a user taps the follow category button of the Clips Player to open the Followable Category screen - a user swipes left from the right edge of the screen on a Clip with a Followable Category and opens the Followable Category screen Whenever a Followable Category Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Enable Captions This event is recorded when: - a user taps the button to enable Closed Captions in the Clips Player Whenever an Enable Captions event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Disable Captions This event is recorded when: - a user taps the button to disable Closed Captions in the Clips Player Whenever a Disable Captions event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `categoryId`, `categoryName`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Shown This event is recorded when: - a user attempts to follow a category but has reached their following limit and the limit dialog is displayed Whenever a Followable Category Limit Shown event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Action Button Tapped This event is recorded when: - a user taps the action button in the followable category limit dialog Whenever a Followable Category Limit Action Button Tapped event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Followable Category Limit Dismissed This event is recorded when: - a user dismisses or closes the followable category limit dialog without taking action Whenever a Followable Category Limit Dismissed event occurs, event data for `clipId`, `clipTitle`, `clipIndex`, `clipCollectionCount`, `collection`, `categories`, `clipFeedType`, `captionsEnabled`, `actionLinkId`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `followableCategoryLimitActionText`, `followableCategoryLimitActionUrl`, `followableCategoryLimitDialogue`, `location`, `eyebrow`, `metadata`, `isMuted` and `context` is returned. ### Muted Clip This event is recorded when a user taps the mute button to mute audio while viewing a Clip. Whenever a Muted Clip event occurs, event data for `captionsEnabled`, `actionLinkId`, `categories`, `categoryDetails`, `clipHasAction`, `clipActionText`, `clipActionUrl`, `clipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `clipId`, `clipIndex`, `clipTitle`, `collection`, `contentLength`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isLive`, `isMuted` and `context` is returned. ### Unmuted Clip This event is recorded when a user taps the mute button to unmute audio while viewing a Clip. Whenever an Unmuted Clip event occurs, event data for `captionsEnabled`, `actionLinkId`, `categories`, `categoryDetails`, `clipHasAction`, `clipActionText`, `clipActionUrl`, `clipActionType`, `clipHasSecondaryActions`, `clipSecondaryActionsText`, `clipSecondaryActionUrls`, `clipSecondaryActionTypes`, `clipId`, `clipIndex`, `clipTitle`, `collection`, `contentLength`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isLive`, `isMuted` and `context` is returned. ## Sheet Events ### Opened Sheet This event is recorded in the following scenarios: - When a user taps on an action button on a clip or story to open a Sheet - When a Sheet is opened via a deep link - When a Sheet is opened via the `openSheet` method Whenever a user taps on an action button on a clip or story to open a Sheet the Opened Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyPlaybackMode`, `currentCategory`, `collection`, `categories`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `clipId`, `clipTitle`, `clipIndex`, `clipHasAction`, `clipActionType`, `clipActionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `metadata`, `isMuted` and `context` is returned. Whenever a Sheet is opened via a deep link or via the `openSheet` method the Opened Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `metadata`, `isMuted` and `context` is returned. ### Dismissed Sheet This event is recorded when a user closes a Sheet by: - Tapping outside of the Sheet (for 50% height setting Sheets) - Dragging the sheet down beyond a threshold - Tapping the back button - Tapping the system back button Whenever a user dismisses the Sheet on a clip or story the Dismissed Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `storyId`, `storyTitle`, `storyIndex`, `storyPageCount`, `storyPlaybackMode`, `currentCategory`, `collection`, `categories`, `pageID`, `pageIndex`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `pageActionUrl`, `clipId`, `clipTitle`, `clipIndex`, `clipHasAction`, `clipActionType`, `clipActionText`, `clipActionUrl`, `captionsEnabled`, `actionLinkId`, `metadata`, `isMuted` and `context` is returned. Whenever a user dismisses the Sheet that is opened via a deep link or via the `openSheet` method the Dismissed Sheet event occurs, event data for `sheetId`, `sheetTitle`, `sheetSize`, `openedReason`, `metadata`, `isMuted` and `context` is returned. ## Card Events ### Card Tapped This event is recorded when: - a user taps on the Storyteller Card view Whenever a Card Tapped event occurs, event data for `cardId`, `cardActionType`, `cardActionUrl`, `cardAspectRatio`, `cardBackgroundType`, `cardCollectionId`, `cardIndex`, `cardSubtitle`, `cardTitle`, `categories`, `categoryDetails`, `hasButton`, and `context` is returned. ### Card Viewed This event is recorded when: - the background image of the card closest to the center of the screen loads. For video cards, this event is triggered when the video starts playing. Whenever a Card Viewed event occurs, event data for `cardId`, `cardActionType`, `cardActionUrl`, `cardAspectRatio`, `cardBackgroundType`, `cardCollectionId`, `cardIndex`, `cardSubtitle`, `cardTitle`, `categories`, `categoryDetails`, `hasButton`, and `context` is returned. ## Ad Events ### Opened Ad #### Stories This event is recorded when: - an ad is loaded because the previous Story finished - a user swipes left on a Story to go to the next Story and an ad appears - a user swipes right on a Story to go to the previous Story and an ad appears Whenever a Opened Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `openedReason`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when: - a user swipes up on a clip to go to the next clip and an ad should appear next - a user swipes down on a clip to go to the previous clip and an ad should appear next - an ad completes a loop and begins playing again from the start Information about Ad View is only sent to client delegates on an OpenedAd event. Whenever a Opened Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `openedReason`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Dismissed Ad #### Stories This event is recorded when a user: - taps close to dismiss the ad - swipes down to dismiss the ad - taps back on their device UI to dismiss the ad (Android only) - taps to skip the ad if the ad is the last Page in the current set of Stories - swipes left on the ad to skip it if the ad is the last Page in the current set of Stories - completes the ad if the ad is the last Page in the current set of Stories Whenever a Dismissed Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `dismissedReason`, `durationViewed`, `pagesViewed`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user: - taps the back button to exit the clips player when an ad is being shown - (Android only) presses the system back button to exit the clips player when an ad is being shown Whenever a Dismissed Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `dismissedReason`, `durationViewed`, `pagesViewed`, `clipsViewed`, `loopsViewed`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Skipped Ad #### Stories This event is recorded when a user: - swipes left to go to the next Story before completing the current ad - taps to go to the next Page on an ad before completing the current ad Whenever a Skipped Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when: - a user swipes up to go to the next clip - a user swipes down to go the previous clip Whenever a Skipped Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Ad Action Button Tapped #### Stories This event is recorded when a user taps on an action button on an ad to open a link. Whenever a Ad Action Button Tapped event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `actionText`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user: - taps the action button at the bottom of an ad displayed in clips - swipes left on a clip to open the relevant action Whenever a Ad Action Button Tapped event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Previous Ad This event is recorded when a user: - swipes right to go to the previous Story when viewing an ad - taps back to go to the previous Page when viewing an ad Whenever a Previous Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. ### Finished Ad #### Stories This event is recorded at the same time as Dismissed Ad, Skipped Ad, Previous Ad and Viewed Ad Page Complete and gives an easier way to determine when an ad finishes for any reason. Whenever a Finished Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded at the same time as DismissedAd, or SkippedAd. Whenever a Finished Ad event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `adView`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Paused Ad #### Stories This event is recorded when a user pauses a Page within an ad by pressing and holding on the Page. Whenever a Paused Ad Page event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is recorded when a user pauses a clip which is an ad by tapping the screen. It does not fire when a clip is paused automatically for sharing or following an action Whenever a Paused Ad Clip event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Resumed Ad #### Stories This event is recorded when a user resumes playing a Page within an ad by releasing their long press which paused the ad. Whenever a Resumed Ad Page event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `pageActionText`, `currentCategory`, `categories`, `pageActionUrl`, `isMuted` and `context` is returned. #### Clips This event is fired when a clip which is an ad is paused and a user taps the screen to resume playback. It does not fire when a clip is resumed automatically Whenever a Resumed Ad Clip event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page First Quartile This event is recorded when a user reaches 1/4 of the way through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page First Quartile event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Midpoint This event is recorded when a user reaches halfway through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Midpoint event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Third Quartile This event is recorded when a user reaches 3/4 of the way through the duration of a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Third Quartile event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Viewed Ad Page Complete This event is recorded when a user reaches the end of the duration for a page or clip which is an ad. For clip ads (which loop), it fires for each loop of the clip. Whenever a Viewed Ad Page Complete event occurs, event data for `advertiserName`, `adId`, `adType`, `adPlacement`, `adFormat`, `adResponseIdentifier`, `pageType`, `contentLength`, `pageHasAction`, `pageActionType`, `clipHasAction`, `clipActionType`, `pageActionText`, `clipActionText`, `pageActionUrl`, `clipActionUrl`, `currentCategory`, `collection`, `clipIndex`, `clipCollectionCount`, `isMuted` and `context` is returned. ### Muted Ad This event is recorded when a user taps the mute button to mute audio while viewing an Ad. #### Stories Whenever a Muted Ad event occurs in Stories, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `adView`, `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyIndex`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyTitle`, `isMuted` and `context` is returned. #### Clips Whenever a Muted Ad event occurs in Clips, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `categories`, `contentLength`, `openedReason`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isMuted` and `context` is returned. ### Unmuted Ad This event is recorded when a user taps the mute button to unmute audio while viewing an Ad. #### Stories Whenever an Unmuted Ad event occurs in Stories, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `adView`, `captionsEnabled`, `categories`, `categoryDetails`, `currentCategory`, `contentLength`, `storyDisplayTitle`, `openedReason`, `storyIndex`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `storyPageCount`, `storyTitle`, `isMuted` and `context` is returned. #### Clips Whenever an Unmuted Ad event occurs in Clips, event data for `advertiserName`, `adId`, `adType`, `adFormat`, `adResponseIdentifier`, `categories`, `contentLength`, `openedReason`, `pageActionText`, `pageActionUrl`, `pageHasAction`, `pageActionType`, `searchFilter`, `searchTerm`, `searchSort`, `clipFeedType`, `collectionClipCount`, `isMuted` and `context` is returned. ### Ad Opportunity This event is recorded when the SDK reaches an eligible ad slot and is about to request an ad. This can happen for: - full-screen ads in Stories - full-screen ads between clips - bottom banner ads in the Clips player Whenever an Ad Opportunity event occurs, additional event data specific to this event includes `playerSessionId`, `adRequestId`, and `adSlotType`. `AdOpportunity` also includes the same contextual ad fields as `OpenedAd` when they are available at opportunity time (for example `storyId`, `pageId`, `storyPlaybackMode`, `categories`, `currentCategory`, `pageActionType`, `pageHasAction`, `pageActionText`, `pageActionUrl`, `clipId`, `clipActionType`, `clipHasAction`, `clipActionText`, `clipActionUrl`). Some differences between `AdOpportunity` and `OpenedAd` are expected by design because they are captured at different moments: `AdOpportunity` is a slot-time snapshot (before the ad request), while `OpenedAd` is captured when the rendered ad is opened. ### Ad Requested This event is recorded when the SDK initiates an ad request. This can happen for: - full-screen ads in Stories - full-screen ads between clips - bottom banner ads in the Clips player Whenever an Ad Requested event occurs, additional event data specific to this event includes `playerSessionId`, `adRequestId`, `adSlotType`, `adUnitId`, and `adSource`. ### Ad Loaded This event is recorded when the SDK successfully loads an ad and considers it available for rendering (even if it is not ultimately shown). This can happen for: - full-screen ads in Stories - full-screen ads between clips - bottom banner ads in the Clips player Whenever an Ad Loaded event occurs, additional event data specific to this event includes `playerSessionId`, `adRequestId`, `adSlotType`, `adUnitId`, `adSource`, `adSourceName`, `adSourceId`, `adSourceInstanceName`, `adSourceInstanceId`, `adAdapterLatencyMillis`, `adMediationGroupName`, `adMediationAbTestName`, and `adMediationAbTestVariant`. ### Ad Failed To Load This event is recorded when the SDK fails to load an ad request (including no-fill). This can happen for: - full-screen ads in Stories - full-screen ads between clips - bottom banner ads in the Clips player Whenever an Ad Failed To Load event occurs, additional event data specific to this event includes `playerSessionId`, `adRequestId`, `adSlotType`, `adUnitId`, `adSource`, `adErrorCode`, `adErrorDomain`, `adErrorMessage`, `adSourceName`, `adSourceId`, `adSourceInstanceName`, `adSourceInstanceId`, and `adAdapterLatencyMillis`. ### Ad Paid This event is recorded when the SDK receives impression-level paid revenue info for an ad. This can happen for: - full-screen ads in Stories - full-screen ads between clips - bottom banner ads in the Clips player Whenever an Ad Paid event occurs, additional event data specific to this event includes `playerSessionId`, `adRequestId`, `adSlotType`, `adUnitId`, `adSource`, `adSourceName`, `adSourceId`, `adSourceInstanceName`, `adSourceInstanceId`, `adAdapterLatencyMillis`, `adValueMicros`, `adCurrencyCode`, and `adValuePrecision`. ### Ad Session Summary This event is recorded when a root Storyteller player session ends (Clips or Stories) and ad session counters are flushed. The summary aggregates paid ads activity for that player session: - `playerSessionId` - `adSessionOpportunitiesCount` - `adSessionRequestsCount` - `adSessionLoadsCount` - `adSessionFailedToLoadCount` - `adSessionPaidCount` - `adSessionRevenueMicros` (optional) - `adSessionCurrencyCode` (optional) - `adSource` (optional) `adSessionRevenueMicros` and `adSessionCurrencyCode` are omitted when mixed currencies are observed or when revenue/currency data is unavailable. ## Playback Events ### Ready to Play This event is called once per video Page at the point when the video player has been loaded. Whenever a Ready to Play event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isLive`, `metadata` and `context` is returned. ### Media Started This event is called once per video Page at the point when the video starts to play for the first time. Whenever a Media Started event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isLive`, `metadata` and `context` is returned. ### Buffering Started This event is called on video Pages whenever the video starts to buffer. Whenever a Buffering Started event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isInitialBuffering`, `isLive`, `metadata` and `context` is returned. ### Buffering Ended This event is called on video Pages whenever the video finishes buffering > Note: There should be at most one Ready to Play event and one Media Started event for every Page. There may be multiple Buffering Started/Buffering Ended pairs of events for an individual Page. There may not always be a Buffering Ended event for every Buffering Started event as the user may choose to exit the Page during buffering. Whenever a Buffering Ended event occurs, event data for `storyId`, `storyTitle`, `storyIndex`, `pageID`, `pageIndex`, `pageType`, `contentLength`, `actionLinkId`, `isInitialBuffering`, `timeSinceBufferingBegan`, `isLive`, `metadata` and `context` is returned. ## Search Events ### Opened Search The openedSearch event is recorded when: - A user taps on Search from a Story within the Story player - A user taps on Search from a Clip within the Clip player ### Dismissed Search The dismissedSearch event is recorded when a user taps the 'X' button to exit the Search interface. ### Performed Search The performedSearch event is recorded when: - A user taps the 'Search' icon after entering a term in the Search bar (whether manually or by tapping the 'arrow' icon beside a Search suggestion to populate the search bar) - A user taps the 'Search' icon beside a Search suggestion. The Search is then performed with the suggestion as the term. - A user taps 'Apply filters' from the filters interface. ### Opened Filters The openedFilters event is recorded when, after a user has performed as Search, they press the 'Filter' icon to bring up the filter interface. > Note that filters can only be applied after the initial search has been performed. ### Dismissed Filters This event is recorded when a user, after calling up the 'Filters' interface, swipes down to exit the interface without applying any. ### Used Suggestion The usedSuggestion event is recorded when: - A user taps the 'arrow' icon beside a Search suggestion to populate the Search bar. This does not trigger any other event, and the user may amend the Search bar input before performing a Search. - A user taps the 'Search' icon beside a Search suggestion. This simultaneously triggers a PerformedSearch event, above. ## Event Data For each event, data is returned with details about the story and Page involved as well as some extra properties with more information about what the user has done. The data is returned as a `UserActivityData` class with the following properties: ### Story ID The `storyId: String?` is the ID of the Story for which the event occurred. ### Story Title The `storyTitle: String?` is the title of the Story for which the event occurred. ### Story Display Title The `storyDisplayTitle: String?` is the display title of the Story for which the event occurred. ### Story Index The `storyIndex: Int?` is the index of the Story for which the event occurred in the row from which it was opened at the point it was opened - this is only included on `OpenedStory` events. Note: this value is 1-based. ### Story Page Count The `storyPageCount: Int?` is the number of Pages in the Story. ### Story Read Status The `storyReadStatus: String?` is whether the Story was read or unread at the point the Story was opened - this is only included on `OpenedStory` events. Note this will either be `read` or `unread`. ### Page ID The `pageId: String?` is the ID of the Page for which the event occurred. ### Page Index The `pageIndex: Int?` is the index of the Page in the Story for which the event occurred. Note: this value is 1-based. ### Page Type The `pageType: String?` is the type of the Page associated with the event. This can have the value `image`, `video` or `poll`. ### Story Playback Mode The `storyPlaybackMode: String?` value states if the Story was opened during the list or in the single Story mode (Storyteller static method.) This is included for all events. The values for this are either `list` or `singleStory`. ### Page Has Action The `pageHasAction: Bool?` value states whether the Page associated with the event contains an action. ### Page Action Type The `pageActionType: String?` is the type of the action on the page. ### Page Action Text The `pageActionText: String?` is the text call to action if the Page has an Action. ### Page Action URL The `pageActionUrl: String?` is the URL for the link if the Page has an Action. ### Opened Reason The `openedReason: String?` value states how the user opened a Story or Clip. The possible values for this are `storyRowTap` (the user tapped the Story in the Story row); `deepLink` (openStory or openPage was called to open the Story); `swipe` (the user swiped left or right to change the current Story); `automaticPlayback` (the user completed the previous Page); `tap` (the user tapped on the previous Page to navigate to this Page); `clipListTap` (the user tapped the Clip in a Clip list) and `loop` (a Clip has completed a full playback cycle and started playing again). `openedReason` is only included on `OpenedStory` and `OpenedClip` events. ### Dismissed Reason The `dismissedReason: String?` value states the way the user dismissed a Story or Clip. The possible values for this are `closeButtonTapped` (the user tapped close to dismiss the Story); `swipedDown` (the user swiped down to dismiss the Story); `swipedFirstStory` (the user swiped the first Story to dismiss it); `swipedFinalStory` (the user swiped the final Story to dismiss it); `skippedFinalPage` (the user tapped to skip the final Page of the final Story); `backTapped` (the user tapped their device back button to dismiss the Story view); `completedFinalPage` (the user completed the final Page of the final Story) and `backButtonTapped` (the user tapped the back button to dismiss the Clip). `dismissedReason` is only included on `DismissedStory` and `DismissedClip` events. ### Duration Viewed The `durationViewed: Float?` is the duration the user viewed the Story or Clip for in milliseconds. This is measured from the most recent `OpenedStory` or `OpenedClip` event with an Opened Reason of `storyRowTap`, `deepLink` or `clipsListTap`. This timer is reset after any `DismissedStory` or `DismissedClip` events. For `FinishedClip`, Duration Viewed is the the duration the user viewed the Clips player for in milliseconds. This is measured from the most recent `OpenedClip` event with an Opened Reason of `swipe`. ### Pages Viewed Count The `pagesViewedCount: Int?` is the total number of Pages a user has viewed since the most recent `OpenedStory` event with an Opened Reason of `storyRowTap` or `deepLink`. This count is reset after any `DismissedStory` events. ### Content Length The `contentLength: Long?` is the total duration of the Page content in seconds. ### Ad ID The `adId: String?` is the ad ID if an event is associated with an ad. ### Share Method The `shareMethod: String?` is the component name of the app which the user has selected for sharing. > Note: on Android API level 31 and above, `shareMethod` is not supported and will be `null` for this event. ### Ad View The `adView: View?` is the view the ad is rendered in. This is included for `OpenedAd` and `FinishedAd`, and may also be included on Clips ad lifecycle events (for example `adLoaded` and `adPaid`) when a view instance is available. ### Advertisers Name The `advertiserName: String?` is the name of the advertiser for a particular ad. This is only included for ad events. ### Ad Type The `adType: String?` tells the source component of the ad. It can be either `stories` or `clips`. This is only included for ad events. ### Ad Placement The `adPlacement: String?` tells the placement of the ad. It can be either `Between Stories`, `Between Pages` or `Between Clips`. This is only included for ad events. ### Ad Format The `adFormat: String?` represents the format of the Ad that was displayed. Possible values can be `customNative`, `native`, `banner`. This is only included for ad events. ### Ad Response Identifier The `adResponseIdentifier: String?` represents the response identifier attached to the ad that was received from the ad provider. Used for debugging ad targeting. This is only included for ad events. ### Ad Source The `adSource: String?` is the ad source label for ad events. Supported values include `gam` and `admob` for Google module integrations. The value may be `null` when the source is not available (for example Storyteller First Party Ads or custom integrating-app ads). ### Player Session ID The `playerSessionId: String?` is the unique identifier for a Storyteller player ad analytics session. ### Ad Request ID The `adRequestId: String?` is the correlation identifier shared by the lifecycle events related to a specific ad request attempt. ### Ad Slot Type The `adSlotType: String?` identifies the ad slot type for `AdOpportunity`, `AdRequested`, `AdLoaded`, `AdFailedToLoad`, and `AdPaid`. Current values are `fullScreen` and `bottomBanner`. ### Ad Unit ID The `adUnitId: String?` is the ad unit identifier reported by the ad source when available. ### Ad Error Code The `adErrorCode: Int?` is the source-specific error code for failed ad requests. ### Ad Error Domain The `adErrorDomain: String?` is the source-specific error domain for failed ad requests. ### Ad Error Message The `adErrorMessage: String?` is the source-specific error message for failed ad requests. ### Ad Source Name The `adSourceName: String?` is the mediation source name for a loaded/failed/paid ad event when available. ### Ad Source ID The `adSourceId: String?` is the mediation source identifier for a loaded/failed/paid ad event when available. ### Ad Source Instance Name The `adSourceInstanceName: String?` is the mediation source instance name for a loaded/failed/paid ad event when available. ### Ad Source Instance ID The `adSourceInstanceId: String?` is the mediation source instance identifier for a loaded/failed/paid ad event when available. ### Ad Adapter Latency Millis The `adAdapterLatencyMillis: Long?` is the adapter latency reported by the ad source, in milliseconds. ### Ad Mediation Group Name The `adMediationGroupName: String?` is the mediation group name reported on Ad Loaded events when available. ### Ad Mediation AB Test Name The `adMediationAbTestName: String?` is the mediation A/B test name reported on Ad Loaded events when available. ### Ad Mediation AB Test Variant The `adMediationAbTestVariant: String?` is the mediation A/B test variant reported on Ad Loaded events when available. ### Ad Value Micros The `adValueMicros: Long?` is the paid revenue value in micros for Ad Paid events. ### Ad Currency Code The `adCurrencyCode: String?` is the ISO currency code associated with `adValueMicros` for Ad Paid events. ### Ad Value Precision The `adValuePrecision: String?` is the precision tier provided alongside paid revenue values for Ad Paid events. ### Ad Session Opportunities Count The `adSessionOpportunitiesCount: Int?` is the total number of ad opportunities counted in the Clips player session summary. ### Ad Session Requests Count The `adSessionRequestsCount: Int?` is the total number of ad requests counted in the Clips player session summary. ### Ad Session Loads Count The `adSessionLoadsCount: Int?` is the total number of loaded ads counted in the Clips player session summary. ### Ad Session Failed To Load Count The `adSessionFailedToLoadCount: Int?` is the total number of failed ad loads counted in the Clips player session summary. ### Ad Session Paid Count The `adSessionPaidCount: Int?` is the total number of paid ad events counted in the Clips player session summary. ### Ad Session Revenue Micros The `adSessionRevenueMicros: Long?` is the total paid revenue in micros for the Clips player session summary when aggregatable. ### Ad Session Currency Code The `adSessionCurrencyCode: String?` is the currency code paired with `adSessionRevenueMicros` when session revenue can be represented in a single currency. ### Is Initial Buffering The `isInitialBuffering: Boolean?` value is returned if the buffering happens at the start of playback for that Page. This is only included for `BufferingStarted` and `BufferingEnded` events. ### Time Since Buffering Began The `timeSinceBufferingBegan: Long?` value is the duration the current buffering lasted for in milliseconds. This is only included for `BufferingEnded` events. ### Categories The `categories: [String]?` value is the list of categories assigned to the Story for which the event occurred. This is only included on `OpenedStory` events. ### Poll Answer The `pollAnswerId: String?` is the ID of the answer the user selected when voting. This is only included on `VotedPoll` events. ### Trivia Quiz Answer ID The `triviaQuizAnswerId: String?` is the ID of the selected trivia quiz answer. This is only included on `TriviaQuizQuestionAnswered` events. ### Trivia Quiz ID The `triviaQuizId: String?` is the ID of the Trivia Quiz that was completed or answered. This is only included on `TriviaQuizQuestionAnswered` and `TriviaQuizCompleted` events. ### Trivia Quiz Question ID The `triviaQuizQuestionId: String?` is the ID of the Trivia Quiz question which was answered. This is only included on `TriviaQuizQuestionAnswered` events. ### Trivia Quiz Title The `triviaQuizTitle: String?` is the title of the Trivia Quiz that was completed or answered. This is only included on `TriviaQuizQuestionAnswered` and `TriviaQuizCompleted` events. ### Trivia Quiz Score The`triviaQuizScore: Int?` value is the score of the trivia quiz that was completed. This is only included on `TriviaQuizCompleted` events. ### Clip ID The `clipId: String?` is the ID of the Clip for which the event occurred. ### Clip Title The `clipTitle: String?`is the title of the Clip for which the event occurred. ### Clip Index The `clipIndex: Int?` is the index of the Clip in the row or grid at the point it was selected or the index of the Clip in the feed at the point it was viewed. ### Tile Index The `tileIndex: Int?` is the one-based index of the Story or Clip tile in the row or grid at the point it became visible. ### Clip Collection Count The `clipCollectionCount: Int?` amount of Clips in the collection. ### Clips Viewed The `clipsViewed: Int?` value is the total number of Clips a user has viewed since the most recent OpenedClip event with an Opened Reason of `clipListTap` or `deepLink`. This count should be reset after any DismissedClip events. ### Loops Viewed The `loopsViewed: Int?` is for DismissedClip, the total number of loops (plays of an individual Clip) a user has viewed since the most recent OpenedClip event with an Opened Reason of `clipListTap` or `deepLink`. This count should be reset after any DismissedClip events. For FinishedClip, the total number of loops. ### Clip Has Action The `clipHasAction: Bool?` value is whether the Clip associated with the event contains a primary action. ### Clip Action Type The `clipActionType String?` is the type of the action on the clip. ### Clip Action Text The `clipActionText String?` is the text call to action if the Clip associated with the event has a primary Action link. ### Clip Action URL The `clipActionUrl: String?` is the URL linked to from the action if a Clip associated with the event has a primary Action. ### Action Class The `actionClass: String?` identifies whether the action button that was tapped is a primary or secondary action. Possible values are `primary` and `secondary`. This is only included for `ClipActionButtonTapped` events. ### Action Index The `actionIndex: Int?` is the 1-based index of the secondary action that was tapped. This is only included for `ClipActionButtonTapped` events when a secondary action is tapped. ### Tapped Clip Action Text The `tappedClipActionText: String?` is the text of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Tapped Clip Action URL The `tappedClipActionUrl: String?` is the URL of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Tapped Clip Action Type The `tappedClipActionType: String?` is the type of the specific action button (primary or secondary) that was tapped. This is only included for `ClipActionButtonTapped` events. ### Has Secondary Actions The `clipHasSecondaryActions: Boolean?` indicates whether the clip has any secondary actions. This is included in all Clip Analytics Events. ### Secondary Actions Text The `clipSecondaryActionsText: [String]?` is an array of all the text CTAs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Secondary Action URLs The `clipSecondaryActionUrls: [String]?` is an array of all the URLs on secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Secondary Action Types The `clipSecondaryActionTypes: [String]?` is an array of all the types of secondary actions for the clip. This is included in all Clip Analytics Events when the clip has secondary actions. ### Is Live The `isLive: Boolean?` set to `true` for Live Clips. ### Action Text The `actionText` property has exactly the same value as "Page Action Text" or "Clip Action Text" and is only reported for Action Button Tapped ### Action Link ID The `actionLinkId: String?` is a unique identifier for the action link associated with pages or clips. This property is included in analytics events where actions are present and allows tracking specific action links across the platform. The action link ID can be used to identify which specific CTA (call-to-action) was interacted with or displayed, enabling more granular analytics and attribution for content performance. ### Collection The `collection` is the ID of the Collection if a Story or Clip is being played from a Collection. ### Category Details The `categoryDetails` is a list of Category Detail objects. This includes the name, ID, type and placement of the Category. ### Category Name The `categoryName` of the category being navigated to or dismissed ### Category ID The `categoryId` of the category being navigated to or dismissed. ### Current Category The `currentCategory` is the Category for the row that is currently being interacted with. The information provided from this is the Category title, ID and placement. This is only included on Story and Ad events. ### Search From The `searchFrom` indicates whether the Search for which the event is recorded was opened from Clips or Stories. ### Is Suggestion The `isSuggestion` indicates whether the Search for which the event is recorded used a suggested Search. > Note that when filters are opened (event), the Is Suggestion value should reflect what was used for the initial search performed before opening filters. ### Initial Input The `initialInput` this property tracks the input at the moment the suggestion was used. ### Search Filter The `searchFilter` property includes the content type and date posted used by search filter. ### Search Sort The `searchSort` field indicates method by which the relevant Story / Clip's search results were sorted. ### Search Term The `searchTerm` by which the Clips / Stories were searched, either entered in the search bar by the user or selected / filled from search suggestions. ### Captions Enabled The `captionsEnabled: Boolean?` value is set to `true` if captions are enabled. ### Is Muted The `isMuted: Boolean?` value indicates the current mute state of the audio. This is set to `true` when audio is muted and `false` when audio is unmuted. This property is included in mute/unmute events for Stories, Clips, and Ads. ### Sheet ID The `sheetId` is the ID of the Sheet for which the event occurred. ### Sheet Title The `sheetTitle` is the title of the Sheet for which the event occurred. ### Sheet Size The `sheetSize` is the height setting of the Sheet. This can have the value `50`, `75` or `100` (representing % of screen height). ### Card ID The `cardId: String?` is the ID of the Card for which the event occurred. This is included for `CardTapped` event. ### Card Action Type The `cardActionType: String?` is the type of the action on the Card. This is included for `CardTapped` event. ### Card Action URL The `cardActionUrl: String?` is the URL for the link if the Card has an action. This is included for `CardTapped` event. ### Card Aspect Ratio The `cardAspectRatio: String?` is the aspect ratio of the Card. This is included for `CardTapped` event. ### Card Background Type The `cardBackgroundType: String?` is the type of background on the Card. This can have the value `image` or `video`. This is included for `CardTapped` event. ### Card Collection ID The `cardCollectionId: String?` is the ID of the collection the Card belongs to. This is included for `CardTapped` event. ### Card Index The `cardIndex: Int?` is the index of the Card in the collection. Note: this value is 1-based. This is included for `CardTapped` event. ### Card Subtitle The `cardSubtitle: String?` is the subtitle text displayed on the Card. This is included for `CardTapped` event. ### Card Title The `cardTitle: String?` is the title text displayed on the Card. This is included for `CardTapped` event. ### Metadata The `metadata: Map?` is a map containing custom metadata key-value pairs associated with the content. This is included for Story, Clip, Playback, and Sheet events. The metadata comes from the Story or Clip associated with the event and can contain custom fields provided via the Storyteller API. ### Followable Category Limit Action Text The `followableCategoryLimitActionText: String?` is the text displayed on the action button in the followable category limit dialog. This is only included for followable category limit events. ### Followable Category Limit Action URL The `followableCategoryLimitActionUrl: String?` is the URL that the action button in the followable category limit dialog will navigate to when tapped. This is only included for followable category limit events. ### Followable Category Limit Dialogue The `followableCategoryLimitDialogue: String?` is the main text content displayed in the followable category limit dialog explaining why the user cannot follow more categories. This is only included for followable category limit events. ### Location The `location: String?` indicates where the followable category limit dialog was displayed. For clip-related limit events, this value is `"On Clip"`. This is only included for followable category limit events. ### Eyebrow The `eyebrow: String?` is the subtitle text displayed above the title for Stories and Clips tiles. It is also present in Stories and Clips Player assuming it is set in CMS. ### Completion Type The `completionType: String?` indicates how the user finished watching a Clip or completed a loop. This property is included in `finishedClip` and `completedLoop` events. Possible values include: - `natural` - The clip or loop completed naturally by playing to the end - `scrubbed` - The user used the progress bar to scrub/seek to the end This helps track whether users are actively engaging with the full content or skipping through it using the progress bar. ### Context The `context: StorytellerAnalyticsContext?` contains custom context data that was provided when configuring the SDK row/grid/card component. This allows integrators to track the context of where the analytics event occurred. This property is included in all Story, Clip, Ad, Media Playback, Sheet, and Card analytics events. --- ## Source: Ads.md # Ads ## Introduction The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager via an SDK extension developed by Storyteller and Ads from other sources via custom implementation provided by the integrator. Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team. ### Choosing an Ad Module The Storyteller SDK provides two ad module options for integrating Google ads: | Feature | StorytellerGamModule | StorytellerAdMobModule | | ------- | -------------------- | ---------------------- | | Custom Native Templates | Supported | Not supported | | Key-Value Pair Targeting | Supported | Supported (via network extras) | | Bottom Banner Ads | Supported | Supported | | Banner Priority Mode | Not supported | Supported | | Use Case | Publishers with GAM accounts | Apps using standard AdMob | **Important**: Only one ad module should be used at a time. Do not register both `StorytellerGamModule` and `StorytellerAdMobModule` simultaneously. The SDK behavior is undefined when both modules are registered. ### Storyteller First Party Ads If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK. ### Storyteller GAM SDK To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller. You will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager. To use this extension, first install it using Gradle. Ensure you have the following Maven repository added to your `settings.gradle`: ```groovy maven { url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk") } ``` Then add the following reference to your version catalog file: ```toml storyteller-ads = { module = "Storyteller:ads", version.ref = "storyteller" } ``` And finally reference this in your `build.gradle`: ```groovy implementation(libs.storyteller.ads) ``` #### Basic Setup Now initialize the extension as follows: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/33813572/storyteller" }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` You will need to supply the following parameters: | Parameter Name | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{ storytellerAdRequestInfo -> adUnitId` } | The lambda which returns desired `adUnitId`. This can be used for dynamic `adUnitId` changes depending on `storytellerAdRequestInfo` content. If you do not need dynamic adUnit changes simply put the static ad unit as a return value. | | `bottomBannerAdUnit` | Optional lambda that returns the Ad Unit ID used specifically for the Clips bottom banner placement. Leave this `null` if you don't plan to serve Clips bottom banner ads. | Then pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance: ```kotlin Storyteller.modules = listOf(storytellerGamModule) ``` ### Showcase examples - [Compose — GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L64) - [Compose — ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L12) - [XML — GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L64) - [XML — ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L12) #### Setup with the dynamic Ad Unit changes Example for dynamic Ad Unit changes when we want to use different adUnit for Stories and Clips: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> when (storytellerAdRequestInfo) { is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories" else -> "/33813572/storyteller/clips" } }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` #### Setup with the additional parameters You can also supply optional parameters `templateIds` and `keyValuePairs` if needed: ```kotlin import com.storyteller.modules.ads.StorytellerGamModule import com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> when (storytellerAdRequestInfo) { is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories" else -> "/33813572/storyteller/clips" } }, templateIds = StorytellerCustomNativeTemplateIds("12102683", "12269089"), keyValuePairs = { emptyMap() }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(storytellerGamModule) //initialize code } ``` | Parameter Name | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `templateIds` | If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct. | | `keyValuePairs` | A function that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. **By default, Storyteller always includes the following KVPs:** `stApiKey`(current API key), `stCollection`(the identifier of the clips collection where the ad will be displayed), `stClipCategories`(list of categories for the current clip associated with the item for ad targeting), `stNextClipCategories`(list of categories for the next clip associated with the item for ad targeting), `stAdIndex`(count of the Ad position from the start of the stCollection). | ### Storyteller AdMob SDK The Storyteller AdMob Module provides integration with standard AdMob ads for apps that do not use Google Ad Manager. This module is an alternative to the GAM module and supports native ads with an optional banner fallback strategy. **Important**: Only use one ad module at a time. Do not use `StorytellerAdMobModule` together with `StorytellerGamModule`. The SDK behavior is undefined when both modules are registered. To use this extension, first install it using Gradle. Ensure you have the following Maven repository added to your `settings.gradle`: ```groovy maven { url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk") } ``` Then add the following reference to your version catalog file: ```toml storyteller-ads = { module = "Storyteller:ads", version.ref = "storyteller" } ``` And finally reference this in your `build.gradle`: ```groovy implementation(libs.storyteller.ads) ``` #### AdMob Basic Setup Initialize the AdMob module as follows: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/yyy" }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` You will need to supply the following parameters: | Parameter Name | Description | | -------------- | ----------- | | `nativeAdUnit` | A lambda function that returns the AdMob ad unit ID for native ads. AdMob ad unit IDs follow the format `ca-app-pub-xxx/yyy`. This is the primary ad type used by the module. | | `bannerAdUnit` | Optional lambda that returns the Ad Unit ID for banner ads used as fallback when native ads fail to load (e.g., no fill). | | `bottomBannerAdUnit` | Optional lambda that returns the Ad Unit ID used specifically for the Clips bottom banner placement. Leave this `null` if you don't plan to serve Clips bottom banner ads. | | `enableBannerAdPriority` | Optional boolean. When `true`, banner ads are attempted first with native ads as fallback. When `false` (default), native ads are attempted first with banner ads as fallback. Only takes effect if `bannerAdUnit` is configured. | | `keyValuePairs` | Optional lambda that returns a `Map` of custom key-value pairs passed to AdMob as network extras for ad targeting. Only sent when `Storyteller.eventTrackingOptions.enableAdTracking` is enabled. | Then pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance: ```kotlin Storyteller.modules = listOf(adMobModule) ``` Example for dynamic Ad Unit changes when you want to use different ad units for Stories and Clips: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> when (adRequestInfo) { is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "ca-app-pub-xxx/stories" else -> "ca-app-pub-xxx/clips" } }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` #### AdMob Setup with Banner Fallback The AdMob module supports a banner fallback strategy to maximize fill rate. When configured, if a native ad request fails (e.g., due to no fill), the module automatically attempts to load a banner ad as a fallback: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" }, bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/banner_fallback" }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` The fallback flow works as follows: 1. The module first attempts to load a native ad using `nativeAdUnit` 2. If the native ad fails to load, and `bannerAdUnit` is configured, it attempts to load a banner ad 3. If both requests fail, the combined error is reported #### AdMob Setup with Bottom Banner Ads To enable bottom banner ads in Clips, provide the `bottomBannerAdUnit` parameter: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" }, bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/bottom_banner" }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` #### AdMob Setup with Key-Value Pairs The AdMob module supports custom key-value pairs for ad targeting. These are passed as network extras to AdMob: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" }, keyValuePairs = { mapOf("customKey" to "customValue", "targeting" to "premium") }, ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` **Note**: Key-value pairs are only sent when `Storyteller.eventTrackingOptions.enableAdTracking` is enabled. If ad tracking is disabled, the KVPs will not be included in the ad request. #### AdMob Setup with Banner Priority By default, the AdMob module attempts to load native ads first, with banner ads as a fallback. You can reverse this priority using `enableBannerAdPriority`: ```kotlin import com.storyteller.modules.ads.StorytellerAdMobModule import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" }, bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/banner" }, enableBannerAdPriority = true, // Try banner first, native as fallback ) } fun initializeStoryteller() { Storyteller.modules = listOf(adMobModule) // initialize code } ``` When `enableBannerAdPriority` is `true`: 1. The module first attempts to load a banner ad using `bannerAdUnit` 2. If the banner ad fails to load, it attempts to load a native ad using `nativeAdUnit` 3. If both requests fail, the combined error is reported **Note**: `enableBannerAdPriority` only takes effect when `bannerAdUnit` is also configured. If `bannerAdUnit` is not provided, native ads will be loaded regardless of this setting. #### AdMob Test Ad Unit IDs For development and testing, use Google's official test ad unit IDs to avoid generating invalid impressions: | Ad Type | Test Ad Unit ID | | ------- | --------------- | | Native Advanced | `ca-app-pub-3940256099942544/2247696110` | | Banner | `ca-app-pub-3940256099942544/6300978111` | Example setup with test ad units: ```kotlin val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { _ -> "ca-app-pub-3940256099942544/2247696110" }, bannerAdUnit = { _ -> "ca-app-pub-3940256099942544/6300978111" }, ) } ``` **Note**: Replace these test ad unit IDs with your production ad unit IDs before releasing your app. #### Key Differences from GAM When choosing between `StorytellerAdMobModule` and `StorytellerGamModule`, consider the following differences: | Feature | StorytellerGamModule | StorytellerAdMobModule | | ------- | -------------------- | ---------------------- | | **Custom Native Templates** | Supported via `templateIds` parameter | Not supported (AdMob limitation) | | **Key-Value Pair Targeting** | Supported via `keyValuePairs` parameter | Supported via `keyValuePairs` (network extras) | | **Ad Loading Strategy** | Unified AdLoader supports multiple formats in single request | Native ad first (or banner first with `enableBannerAdPriority`), then fallback | | **Banner Priority Mode** | Not supported | Supported via `enableBannerAdPriority` | | **Bottom Banner Ads** | Supported | Supported | | **Ad Unit Format** | GAM format: `/network/ad_unit` | AdMob format: `ca-app-pub-xxx/yyy` | **When to use AdMob Module**: - Your app uses standard AdMob and does not have a Google Ad Manager account - You do not need custom native ad templates - You want configurable ad loading priority (banner vs native first) - You want a simpler integration with automatic banner fallback **When to use GAM Module**: - You have a Google Ad Manager account - You need custom native ad templates configured with the Storyteller Delivery team - You need unified ad loading with multiple formats in a single request - You need the default set of Storyteller KVPs automatically included (e.g., `stApiKey`, `stCollection`, `stClipCategories`) ### Bottom Banner Ads The Clips Player supports bottom banner ads rendered as standard banner views displayed below the video content. Both the GAM module and AdMob module support bottom banner ads. Supply `bottomBannerAdUnit` in the `init()` method to fetch inline adaptive banners for the Clips bottom banner placement. Only 300x50 and 320x50 ad sizes are supported. Example setup with bottom banner ads using GAM: ```kotlin val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply { init( adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/ad_unit_id" }, bottomBannerAdUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/bottom_banner_ad_unit_id" }, ) } ``` Example setup with bottom banner ads using AdMob: ```kotlin val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply { init( nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" }, bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/bottom_banner" }, ) } ``` ### Ad Source for Custom Modules If your custom `StorytellerModule` provides Google ads, set `googleAdSource` to declare the source used by your implementation. - Use `StorytellerGoogleAdSource.GAM` for Google Ad Manager modules. - Use `StorytellerGoogleAdSource.ADMOB` for Google AdMob modules. - Use `null` for non-Google ad providers. Setting `googleAdSource` enables consistent Google paid ad analytics source values and allows the SDK to normalize multiple Google modules safely. `StorytellerGamModule` and `StorytellerAdMobModule` set `googleAdSource` automatically. ### Ad Request Information #### StorytellerAdRequestInfo The `StorytellerAdRequestInfo` sealed class has two subclasses: **`ClipsAdRequestInfo`** Used when an Ad is requested for display in a Clips Player. The properties include: - `collection` - The identifier of the clips collection where the ad will be displayed - `nextClipCategories` - List of categories for the next clip associated with the item for ad targeting - `adIndex` - Count of the Ad position from the start of the Collection - `itemInfo` - Metadata about the item context for ad targeting **`StoriesAdRequestInfo`** Used when an Ad is requested for display in a Stories Player. The properties include: - `placement` - The identifier of the stories placement where the ad will be displayed - `categories` - List of category identifiers for ad targeting and filtering - `adIndex` - Count of the Ad position from the start of the Collection - `itemInfo` - Metadata about the item context for ad targeting #### ItemInfo Each request class includes an `ItemInfo` object that contains: - `categories` - List of categories associated with the item for ad targeting ## Non Skippable Ads Our Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS. --- ## Source: StorytellerBrightcove.md # Storyteller Brightcove Module ## Introduction The Storyteller SDK supports Brightcove analytics as a separate module. This module is designed to work with the Brightcove to provide analytics for videos. ### Setup To setup the module additional dependency needs to be added and the module needs to be initialized and included in `Storyteller.modules` ```groovy maven { url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk") } ``` Then add the following reference to your version catalog file: ```toml storyteller-brightcove = { module = "com.storyteller.brightcove:analytics", version.ref = "1.2.0" } ``` And finally reference this in your `build.gradle`: ```groovy implementation(libs.storyteller.brigthcove) ``` To use the module Storyteller SDK version should be set to `10.6.3` or higher. Now initialize the module as follows: ```kotlin val configuration = StorytellerBrightcoveModuleConfiguration( accountId = "your-account-id", playerName = "your-player-name", source = "your-source", destination = "your-destination-url", ) val storytellerBrigthcoveModule = StorytellerBrightCoveModule(configuration) fun initializeStoryteller() { Storyteller.modules = listOf(storytellerBrigthcoveModule) //initialize code } ``` ## Showcase examples - [Compose — Brightcove module config (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L83) --- ## Source: OpenSourceLicenses.md # Open Source Licenses The Storyteller SDK uses open source libraries to accomplish some of its functionality. Those libraries are: - [Retrofit](https://github.com/square/retrofit) - [ExoPlayer](https://github.com/google/ExoPlayer) - [Coil](https://coil-kt.github.io/coil/) - [OkHttp](https://square.github.io/okhttp/) These libraries are all subject to the [Apache 2.0](https://github.com/square/retrofit/blob/master/LICENSE.txt) open source license. According to the terms of the license, it may be necessary to include a statement of this license somewhere in the app which integrates the SDK. You should consult your own legal adviser if you are unsure. The text of the license is included below: ```text Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` --- ## Source: Changelog.md # Changelog ## **11.3.2** - 06/04/26 ### Bug Fixes - Fixed Stories onboarding/instruction screens repeating on every player open when `enableRemoteViewingStore=false`. ## **11.3.1** - 01/04/26 ### Bug Fixes - Reverted the default `lists.title` theme sizing change, restoring `textSize` to `22` and `lineHeight` to `28`. ## **11.3.0** - 26/03/26 ### Breaking Changes - Removed `offset` from `StorytellerStoriesDataModel`, `StorytellerClipsDataModel`, and the View `ListConfiguration` APIs. ### New Features - Added support for AdMob native ads in Stories. - Added `theme.player.clips.title` to customize the Clips player title with `font`, `textSize`, `lineHeight`, and `textColor` properties. - Added `theme.player.clips.actionIconSize` to control the size (in dp) of clip player action icons. - Added `theme.player.icons.mute` and `theme.player.icons.captions` for stateful mute/captions icon overrides in the player. ### Improvements - Improved row accessibility by making list rows scale with system font size when using automatic rows mode. - Added `googleAdSource` support for custom `StorytellerModule` implementations. - Migrated SDK annotation processing from kapt to KSP while keeping Kotlin on `1.9.0`, which removes the old Java 17-only kapt workaround and restores JDK 21 build compatibility. - Updated `StorytellerAnalyticsContext` documentation to reflect the current public type alias shape: `Map`. - Extended Cards theme support to allow light/dark text color overrides and text panel backgrounds in Cards with text bellow the content. - Added `cardVideoStarted`, `cardVideoHeartbeat`, and `cardVideoCompleted` analytics events for video cards. `cardVideoHeartbeat` and `cardVideoCompleted` reuse `durationViewed` to report active playback duration. - Added Stories support for the new Google ad analytics events: `adOpportunity`, `adRequested`, `adLoaded`, `adFailedToLoad`, `adPaid`, and `adSessionSummary`. - Added support for `externalApp` actions in Custom Native Ads. - Added `enableFollowableCategorySwipeFromRightEdge` player theme property to allow disabling the right-edge swipe gesture to open Followable Categories. ### Bug Fixes - Fixed Clips feed refresh and pagination issues across For You, Following, and fresh-feed loading. - Fixed duplicate deeplink navigation after dismissing stacked search flows. - Fixed card video playback when a reordered collection starts with a video card. - Fixed Clips followable icons with `ActionType.NONE` so the icon stays visible but does not open the followable screen or fire followable-category tap analytics. - Fixed clip player chip alignment and tablet story previous-tap hit zones. - Fixed banner story pages layout after tablet rotation. ## **11.2.3** - 05/03/26 ### Bug Fixes - Fixed Card playback video issue. ## **11.2.2** - 16/02/26 ### New Features - Added `theme.player.enableFollowableCategorySwipeFromRightEdge` to control opening Followable Category with a right-edge swipe in Clips (default `true`). ### Bug Fixes - Fixed Echo Show support for FireTV SDK. ## **11.2.1** - 13/02/26 ### New Features - Added support for AdMob banner ads in Clips bottom banner placements. - Added support for AdMob native ads in Clips full-screen placements. - Added new Clips ad analytics events: `adOpportunity`, `adRequested`, `adLoaded`, `adFailedToLoad`, `adPaid`, and `adSessionSummary`. - Added `adSource` and mediation metadata fields to ad analytics payloads for Google ad lifecycle events. ### Bug Fixes - Fixed AdChip overlap in Ads in Clips. - Fixed blank empty-state action button in Following tab. - Fixed duplicate ad analytics events triggered by a single user action. ## **11.2.0** - 26/01/26 ### New Features - Added support for `readOrderingMode` (`Basic`, `StartedAware`) from the backend Stories responses. When `StartedAware` is set, rows order stories as NeverOpened → InProgress → Completed while retaining pinned/live/sort ordering. Default remains `Basic` for backward compatibility when the field is absent. - Added clip preloading APIs for first open to reduce initial load time for Clips collections. ### Bug Fixes - Fixed native video ads in legacy GAM starting paused with a black screen. - Fixed native image ads rendering incorrectly with landscape creatives. - Fixed Like/Mute buttons becoming unresponsive after repeated taps. - Fixed followable icon tap responsiveness on Samsung S25 devices. - Fixed followable category screen theme flicker on first open in dark mode. ## **11.1.0** - 07/01/26 ### Breaking Changes - Updated `StorytellerTileType` in `onTileTapped` callback: + `StorytellerTileType.Story.categories` changed from `List` to `List` for richer category details + `StorytellerTileType.Clip` now includes a `categories: List` property ### New Features - Added support for showing Banner Ads at the bottom of the Clips Player. See [Ads](Ads.md#bottom-banner-ads) for more information. - Added support for Non-Skippable Ads in both Stories and Clips Players. See [Ads](Ads.md#non-skippable-ads) for more information. - Added ability to hide Share/Like numbers via Settings Theme configuration - Added support for custom 'Live' chip text across Clips and Stories ### Improvements - Improved gesture zones for the Clip Progress Bar scrubbing experience - Improved Story Action Button widths to follow industry standards - Improved native video Ads duration reporting accuracy - Improved volume handling - device volume 0 is now treated as muted state - Improved theme usage for Clips buttons (primary button, For You/Following empty state button) ### Bug Fixes - Fixed tablet UI misalignment and overlapping issues - Fixed sharing a Clip from Following or navigated category opening black screen with infinite loading - Fixed `excludeFromAnalytics` returning false when `enableStorytellerTracking` and `enableRemoteViewingStore` are set to false - Fixed incorrect Clip displaying when swiping from viewed Clip to not viewed Clip during paging - Fixed captions state becoming inconsistent when changed on Search screen - Fixed Story Ads progress bar remaining in Ad state when navigating back from image Ad - Fixed Ad chip showing incorrectly after navigating back from Ad to Story page - Fixed `shareButtonTapped` events being incorrectly sent for Stories when `enableStorytellerTracking` is set to `false` - Fixed backgrounding the app breaking the reloading functionality for Stories and Clips - Fixed Following category reporting as NULL in analytics - Fixed Clip video Ads continuing to play audio when app is backgrounded - Fixed tablet Story Ads displaying Live Story chip on banner and image Ads - Fixed sound not behaving correctly in mute/vibration mode - Fixed video Ad audio continuing and mute button appearing briefly on image Story page - Fixed dynamic rows not displaying placeholders fully when wrong category/collection IDs are used - Fixed progress bar failing to scrub after network reconnection - Fixed device mute settings not updating mute state after manual toggle - Fixed deeplinks to Sheets opening with error - Fixed quiz questions being skippable without answering when opened from action or open method - Fixed mute state not updating properly when video Story opened via Search - Fixed closing animation missing when Story/Clip Player is opened from Story Action Button - Fixed Player being dismissed when bottom Sheets action is tapped - Fixed Story image Ads breaking page navigation - Fixed progress bar staying at enlarged state if recents opened during scrubbing - Fixed device volume buttons incorrectly muting standard video Ads - Fixed Instructions Screen showing incorrectly on sign out/in - Fixed most answered polls resetting after navigating between pages - Fixed video poll pages not having mute toggle - Fixed mute button briefly appearing and disappearing when skipping pages with standard Ad - Fixed bounce animation missing on single Clip scroll opened from a Card - Fixed Card with invalid collection opening a black Clip Player - Fixed share module for secondary actions not working - Fixed Following Tab empty state Action Button having no text - Fixed deep linking to Clips beyond the first 50 in a collection via URL scheme - Fixed Sheets opened from Ad actions not considering viewport property ## **11.0.6** - 17/12/25 ### Bug Fixes - Fixed setting the device in silence mode and voluming up doesn't sound Stories ## **11.0.5** - 15/12/25 ### New Features - Added full support for FireTV and AndroidTV ## **11.0.4** - 26/11/25 ### Bug Fixes - Fixed Embedded Clips backgrounding the app when reload button is double-tapped in navigated category - Fixed memory leaks - Fixed action buttons not opening Clip Player properly - Fixed tile animations not working after Story and Clip Player is closed in More Screen - Fixed Cards not sorting by taps correctly - Fixed clickable area for image Ads in Native Ad Clips where tapping the title area incorrectly triggered the action button ## **11.0.3** - 21/11/25 ### Bug Fixes - Fixed action buttons on custom native ads not responding to taps or swipe-up gestures - Fixed external links intermittently failing to open in Android WebView - Prevented the screen from dimming or turning off while watching For You/Following content ## **11.0.2** - 13/11/25 ### Bug Fixes - Fixed issue where standard video ads would freeze after navigating between custom image ads on Story pages ### Improvements - Moved mute button to right-hand rail in Clips player for improved UI consistency - Updated action button widths to follow industry standards - buttons no longer span full width. Button width type is now configurable via CMS and received from backend, allowing per-action customization ## **11.0.1** - 29/10/25 ### Bug Fixes - Fix potential crash which could occur when presenting Native Ads ## **11.0.0** - 22/10/25 ### Breaking Changes - **Standardized public API with `Storyteller` prefix**: All public classes now include the `Storyteller` prefix for consistency. Key renamings include: + `UserInput` → `StorytellerUserInput` + `UserActivity` → `StorytellerUserActivity` + `Category` → `StorytellerCategory` + `Error` → `StorytellerError` + `Environment` → `StorytellerEnvironment` + `DisabledFunctionalFeature` → `StorytellerDisabledFunctionalFeature` + `UserAttributes` → `StorytellerUserAttributes` + `CustomNativeTemplateIds` → `StorytellerCustomNativeTemplateIds` + `TileType` → `StorytellerTileType` + `FollowableCategoryCustomScreen` → `StorytellerFollowableCategoryCustomScreen` - `eventTrackingOptions` can now only be set during SDK initialization by passing a `StorytellerEventTrackingOptions` object to the `initialize` method. Runtime modifications are no longer supported. The property remains publicly readable via `Storyteller.eventTrackingOptions`. To change tracking options, you must reinitialize the SDK. See [Privacy and Tracking](PrivacyAndTracking.md) for more information - Removed deprecated `Storyteller.currentUserId` method for VPPA compliance - Removed `showVoteCount` property from `EngagementTheme` for Polls - Removed `show` property from `TitleTheme` - Removed default `isScrollable` parameter from `StorytellerClipsGrid` and `StorytellerStoriesGrid` - Removed all VPPA-related deprecated methods ### New Features - Added support for custom `context` data in analytics events. Context allows integrators to provide additional analytics attribution data for Storyteller rows, grids, cards, home screens, and embedded clips. For details, see [Context](Analytics.md#context). - Added **Progress Bar for Clips Player** with scrubbing/seeking functionality and analytics events (`scrubbedClip`, completion type tracking) - Added **Stories Captions** support with CC button, enabling/disabling events, and global state persistence - Added **Configurable Default Mute State** with persistent mute preferences - Added **Buttons for Cards** with display, theming, and analytics support - Added **Long Engagement Unit Answers** supporting multi-line text display in Polls and Quizzes - Enhanced **Following** behavior with new public API methods and conditional logic based on `followingFrom` setting - Added automatic user attribute management for followed categories where Storyteller is handling all following logic, without the need for storing and syncing followed categories app-side. See [Following Model](Users.md#storyteller-managed-following) for more information - Added **Card Viewed/Tapped Ordering** for improved card content organization - Added `actionLinkId` property to analytics events with page/clip actions - Added story/clip context properties to ad analytics events - Added `adFormat` support for First-Party ads in Stories - Added `completionType` property to `finishedClip` and `completedLoop` events - Added `captionsEnabled` property to all Story analytics events - Added `hasButton` property to all Card analytics events - Added native back button support for webview navigation ### Bug Fixes - Fixed analytics event issues including missing properties and incorrect triggers - Fixed `openedCategory` event not recorded when navigating back from another category - Fixed `finishedClip` event not sent when switching between For You and Following tabs - Fixed Story page eyebrow and timestamp overlap - Fixed action buttons not being interactable on tablets - Fixed captions not displaying correctly for long text - Fixed Story player dismissing automatically when opened from More screen on Compose - Fixed embedded clips top-level back button not working - Fixed follow state not syncing between Clips page and Followable Category page - Fixed `canGoBack` always returning false when navigating subcategories on Following tab - Fixed theme not applying to rows/grids on bottom sheet - Fixed numerous For You/Following tab navigation and reload issues - Fixed clip ads sometimes switching shortly after appearing - Fixed various UI issues with Stories, Clips, and Ads players ### Improvements - Improved accessibility support for large font sizes - Enhanced ad preloading for Clips - Improved theme handling and persistence - Updated public interface standardization across iOS and Android platforms ## **10.13.10** - 02/10/25 ### Breaking Changes - Added new parameter `offset` to `StorytellerClipsDataModel`, `StorytellerStoriesDataModel`, `ListConfiguration` see [StorytellerListViews.md](StorytellerListViews.md) and [StorytellerLists.md](StorytellerLists.md) ### Bug Fixes - Fixed tiles reload behavior - Fixed Clips duplications - Disabled custom params when ad tracking is disabled ## **10.13.9** - 24/09/25 ### Bug Fixes - Fixed tiles not auto-reloading properly - Fixed audio focus not being requested when unmuting - Fixed incorrect grid display limitations - Fixed text font styling issues in Clip player ## **10.13.8** - 09/09/25 ### Bug Fixes - Fixed double calling of `onDataLoadCompleted` callback - Fixed placeholders not hiding when item count is 0 - Fixed `inApp` action not opening properly - Fixed webView opening issues ## **10.13.7** - 27/08/25 ### Bug Fixes - Fixed an issue with `onDataLoadCompleted` returning incorrect data count ## **10.13.6** - 25/08/25 ### Bug Fixes - Fixed crashes when theme is null in Embedded Clips ## **10.13.5** - 18/08/25 ### Improvements - Improved support for privacy flags ### Bug Fixes - Fixed crashes when swiping through stories ## **10.13.3** - 14/08/25 ### Improvements - Improved support for privacy flags ## **10.13.2** - 11/08/25 ### Bug Fixes - Fixed black screen appearing randomly when scrolling fast between Clips - Fixed order of navigation elements for category navigation inside Clips - Fixed Clip looping issue when swiping to next clip during PTR animation - Fixed `OpenedClip` event issues - Fixed `DismissedClip` event issues - Fixed `OpenedCategory` event not being recorded when opening Clips Player via category methods - Fixed `DismissedCategory` event missing properties when returning using back button - Fixed `FinishedClip` event issues - Fixed Banner/Native Ad action triggering when fast skipping pages - Fixed mute state issues with device volume changes after muting from category navigation - Fixed `collectionClipCount` always being 0 for Muted/unmuted events in FY/F collections - Fixed Title and Eyebrow alignment when Mute toggle enabled - Fixed like status/share number not resetting after PTR or Reload if staying on same clip - Fixed duplicate Clip Ad requests for the initial index - Fixed missing `adIndex` property on ad events - Fixed followable category screen when opening it on tablet landscape - Fixed share/like issues when respective features disabled - Fixed crashes when swiping through stories - Fixed `skippedAd` and `finishedAd` events triggered without ad display when skipping clips quickly - Fixed followable category screen appearing on Clip Ads - Fixed Share and Action Buttons shifting downwards when swiping through Stories - Fixed Eyebrow text positioning with different theme settings - Fixed category title ellipsis display on Clip player - Fixed `DismissPlayer` event timing issues - Fixed crashes when opening `StorytellerEmbeddedClipsFragment` (XML) ### New Features - Added `adIndex` KVP support for Stories and Clips when requesting Ads ### Improvements - Updated FollowableCategory Status request with per-category granularity ## **10.13.1** - 07/08/25 ### Bug Fixes - Fixed if search is remotely disabled in the CMS, the search button still appears on Clips ## **10.13.0** - 30/07/25 ### Breaking Changes - Added `onTileTapped` callback to `StorytellerListViewDelegate` for enhanced tile interaction tracking, see [StorytellerDelegates](StorytellerDelegates.md) - Relaxed public `openDeepLink` method activity requirement from `AppCompatActivity` to `Activity` for better compatibility ### New Features - **Clips Player is now rewritten fully in Compose for better performance and user experience** - **Added support for For You/Following tabs for Clip Rows and Grids** - Added Mute Toggle functionality for Stories and Clips with comprehensive analytics see [Analytics](Analytics.md): + Added muted/unmuted events for Stories, Clips, and Ads + Added `isMuted` property to all Story, Clip, and Ad analytics events - Added Followable Category Limits feature with analytics events, see [Analytics](Analytics.md): + Added Followable Category Limit Shown event + Added Followable Category Limit Action Tapped event + Added Followable Category Limit Dismissed event - Added Story Eyebrow and Clip Eyebrow support - Added Independent Configuration of Analytics Features, see [PrivacyAndTracking](PrivacyAndTracking.md) - Enhanced Ad Placement Algorithms for Stories and Clips - Added `metadata` filed to analytic events ### Bug Fixes - Fixed multiple issues related to previous Clip Player implementation based on Android Views - Fixed multiple issues related to For You/Following Tabs navigation and behaviour - Fixed multiple issues related to the Clips playback - Fixed Story/Clips player action buttons to properly listen to `theme.buttons.textCase` configuration ## **10.12.1** - 10/07/25 ### Breaking Changes - Updated Kotlin version from `1.7.20` to `1.9.0` ## **10.12.0** - 30/06/25 ### New Features - Added new option to Event Tracking Options called `enableRemoteViewingStore` for VPPA compliance. See [PrivacyAndTracking](PrivacyAndTracking.md) for details. ### Improvements - Cards are now using `Activity` instead of `AppCompatActivity` ### Bug Fixes - Fixed Cards video background freezing when backgrounding and foregrounding - Fixed issue with Storyteller Lists height - Fixed Sheets being dismissed automatically when rotating the device on tablet - Fixed handling of `intent://` redirects inside the internal WebView - Fixed crash when opening Clips with Secondary Actions - Fixed CardViewed events not triggering for Cards with image backgrounds - Fixed rare crash in GAM module for Custom Native Ads ## **10.11.0** - 16/06/25 ### Breaking Changes - Implemented VPPA compliance measures including SHA256 hashing for User IDs for requests and removal of Content Id from ad requests. See [PrivacyAndTracking](PrivacyAndTracking.md) for details. - Deprecated `Storyteller.currentUserId` method as part of privacy enhancements. See [Users](Users.md) for updated user management methods. - Added `enableFullVideoAnalytics` flag to `eventTrackingOptions` for granular video analytics control. See [Analytics](Analytics.md) for details. ### New Features - Introduced **Cards** feature - a new content type that can be displayed alongside Stories and Clips with full theming support, personalization, targeting, and analytics. See [Cards](Cards.md) for event details. - Added **Secondary Actions** support for both Stories and Clips, allowing multiple action buttons per content item. See [Analytics](Analytics.md) for additional fields added. - Cards support video playback based on visibility and include comprehensive analytics events (`Card Tapped`, `Card Viewed`). See [Analytics](Analytics.md) for event details. - Enhanced analytics with new properties for Secondary Actions and Cards integration. See [Analytics](Analytics.md) for complete event documentation. ### Improvements - Added methods to retrieve count of clips in a collection and number of Stories in categories. See [AdditionalMethods](AdditionalMethods.md) for details. - Enhanced Embedded Clips to support their own custom themes. See [StorytellerClipsFragment](StorytellerClipsFragment.md) and [Themes](Themes.md) for details. - Added analytics event for 'Story Instructions Screen Viewed'. See [Analytics](Analytics.md) for details. - Improved poll vote count display with per-poll configuration support - Added support for custom tab titles in For You/Following tabs. Configurable via CMS - Improved VPPA compliance by removing direct user identifiers from various API requests. See [PrivacyAndTracking](PrivacyAndTracking.md) for details. ### Bug Fixes - Fixed live story progress bar jumping issues on single-page stories - Resolved clip playback getting stuck when navigating between categories - Fixed search functionality issues including keyboard behavior and story navigation. See [Search](Search.md) for details. - Corrected chip positioning for live clips and ads - Fixed deep linking issues for clips and stories. See [DeepLinking](DeepLinking.md) for details. - Improved pull-to-refresh behavior across different screens - Fixed tablet-specific layout issues including timestamp positioning - Resolved various analytics event property issues. See [Analytics](Analytics.md) for updated event documentation. ## **10.9.5** - 29/05/25 ### Bug Fixes - Fixed an issue where external app links were not opened correctly with ST internal WebView - Fixed an issue where edge to edge layouts were not correctly supported on Android 15+ ## **10.9.4** - 24/04/25 ### Bug Fixes - Fixed an issue with `onDataLoadCompleted` returning incorrect data count - Fixed an issue where disabling `theme.tiles.title.show` property could cause Round Storyteller Lists to disappear ## **10.9.3** - 18/04/25 ### Bug Fixes - Fixed an additional issue with opening a deeplink using externalId ## **10.9.2** - 15/04/25 ### Bug Fixes - fixed open a deeplink with externalId ## **10.9.1** - 10/04/25 ### Improvements - added `collectionClipCount` property to Clip Ad analytics events to reflect the total number of Clips in the Collection, see [Analytics](Analytics.md) for more details ### Bug Fixes - updated the Constraint Layout dependencies to avoid NoMethodFoundError crashes ## **10.9.0** - 6/04/25 ### Breaking Changes - Compose BoM dependency updated to `2024.12.01` - The `openDeepLink` method now requires an `AppCompatActivity` instead of an `Activity`. Please update your calls accordingly. See [DeepLinking](DeepLinking.md) for more details. ### New Features - Introduced **Sheets**, a new way to display web content within the SDK experience. Sheets can be triggered via action buttons or opened directly. See [AdditionalMethods](AdditionalMethods.md#opensheet) and [DeepLinking](DeepLinking.md#sheet) for details. - Added analytics events for `OpenedSheet` and `DismissedSheet`. See [Analytics](Analytics.md). ### Improvements - Added support for respecting Ad Tracking Preferences in the Storyteller SDK and GAM Module. See [PrivacyAndTracking](PrivacyAndTracking.md) for details. - Ensured the `clipIndex` property on Clip analytics events correctly reflects the index within the overall Collection when using Paging. - Corrected the calculation and reporting of the `durationViewed` property in analytics events. - Addressed memory leaks related to Sheets and the Story Pager. ### Bug Fixes - Fixed an issue where the progress bar could disappear on native page ads when tapped quickly. - Fixed a bug preventing muting a story via the volume button if it was previously unmuted. - Resolved an issue where old content briefly flashed after unfollowing a team/category from the clip player and refreshing. - Fixed search results not updating to an empty state when modifying a valid search term to an invalid one. - Corrected behavior where unfollowing items in the "For You" tab did not update the "Following" tab. - Fixed video playback resuming from the stopped point instead of restarting when navigating between video and image pages. - Resolved an issue where the clip player could continue playing audio in the background after pressing the device back button. - Fixed deep links with a correct page ID but incorrect story ID failing to open the player. - Fixed story page elements being removed after viewing native image ads. - Addressed an issue where the first clip ad might appear later than the configured frequency. - Made captions enablement a user-specific setting rather than device-wide. - Fixed crashes related to ConstraintLayout dependencies. - Fixed a crash that could occur on the "More" screen within a clip collection. - Resolved a clip mismatch issue after viewing a "Not Viewed" clip via search. - Fixed users being unable to close Sheets with a swipe gesture over the WebView area. - Corrected status bar icon visibility when Sheets are opened in light mode. - Addressed issues with video page requests not completing in specific environments. - Fixed an issue where tapping the first clip thumbnail opened the wrong clip player if all clips on the initial page were already read (Paging). - Fixed the story player freezing when opening a story while a specific video type (Game Recap) was playing. ## **10.8.0** - 11/03/25 ### Breaking changes - Adding `bottomSheetScreen` functionality. [StorytellerDelegates](StorytellerDelegates.md) section Note that if you are not using this functionality then simply return the default value for this method in your Delegate implementation. ### New Features - Added Clips Paging functionality to Clips Player to improve performance and user experience - Adding support for deep-links via external ID URLs ### Improvements - Updated `collectionClipCount` property on Clip analytics events to be the total number of Clips in the Collection - Added `clipIndex` property to all Ad analytics events and `captionsEnabled` to all Clips analytics events - Improved ad loading performance and preloading logic - Improved loading behavior for embedded Clips. - Updated behavior for deep links with Clips Paging functionality ### Bug Fixes - Fixed a crash occurring when fast-swiping between Clips. - Fixed an issue where Clip progress was not saved when the app was backgrounded. - Fixed incorrect event logging for deep-linked Clips. - Fixed an issue where multiple search filter menus appeared when tapping quickly. - Fixed an issue with inconsistent font rendering on certain devices. - Fixed an issue where users could not tap to go to the next page if it was a page ad. - Fixed an issue where the first tap after reopening a paused native ad was not responsive. - Fixed playback getting stuck when navigating and skipping too fast. - Fixed a lifecycle bug where sound plays when the app is swiped to the background. - Fixed an issue where a shared quiz link navigates the user to the end of the quiz results instead of the beginning. - Fixed an issue where Stories tiles could not be set to round on Compose screen. - Fixed incorrect event property for `clipsViewed` event on Clip Close events. - Fixed an issue where dismissing a story with a CTA button does not auto-reload that story. - Fixed an issue where PTR stops working on clips after multiple fast PTR attempts. - Fixed an issue where followable category screens send two identical paging requests. - Fixed an issue where tapping on a clip while swiping to the followable category screen was possible. - Fixed an issue where auto-scroll occurred while the player was open and the app was backgrounded. - Fixed an issue where the store action button was not opening the Play Store. - Fixed playback issues after category navigation. - Fixed issues with search displaying incorrect or incomplete results. - Fixed an issue where playback was stuck when opening a page ad. - Fixed an issue where category navigation opens a black screen with the sound of the previous clip. - Fixed an issue where opening a clip from the followable category screen freezes the page. - Fixed an issue where a grey line appeared on image poll answers. - Fixed an issue where swiping to other embedded clips got stuck with a reload button. - Fixed an issue where sound continued playing after dismissing a clip on the search/category screen. ## **10.7.6** - 27/02/25 ### Bug Fixes - fixed an issue with crash when rotating Embedded Clips on Samsung tablets ## **10.7.5** - 31/01/25 - fixed an issue with a wrong position for Clip captions - fixed some issues with Ads which resulted in content being cut on some devices - fixed issue with insets handling on Android 15 for internal WebView ### Bug Fixes - fixed insets for both normal clips and embedded clips for both xml and compose implementations - fixed issue where Link to open Story without Page Id fails ## **10.7.4** - 16/01/25 ### Bug Fixes - fixed insets for both normal clips and embedded clips for both xml and compose implementations - fixed issue where Link to open Story without Page Id fails ### Improvements - `reloadData()` method on `StorytellerEmbeddedClips` has no effect when embedded clips is on the custom followable category screen - added missing theme handling for gradient when using `from` infix methods for circular tiles ## **10.7.3** - 24/12/24 ### Bug Fixes - fixed a bug with SHARE_SUCCESS event not being sent when sharing a Clip ## **10.7.2** - 20/12/24 ### Improvements - `dismissPlayer` method now has an optional `onCompletion` callback that is called when the player is dismissed. Please, see the [Additional Methods](AdditionalMethods.md) for more details ## **10.7.1** - 19/12/24 ### New Features - Enabled displaying the exact number of tiles per row on various screens. Please, see the `visibleTiles` property in the "Setting categories and collections, applying themes and styles" section of the [StorytellerCompose](./StorytellerListViews.md) for more details. - Enabled changing the text casing for "Navigating Stories" header on the Instruction screen. Please, see `theme.instructions.headingTextCase` [Themes](./Themes.md#instructions) for more details. - Implemented custom fonts for the Instructions screens headers. Please, see `theme.instructions.headingFont` [Themes](./Themes.md#instructions) for more details. - Added support for chip borders. Please, see the updated section [Themes](./Themes.md#story-and-clip-tiles) for more details. ### Bug Fixes - Fixed some issues related to working with themes for Row. - Fixed the `shareSuccess` event not being sent on Android 10. - Fixed a crash on Storyteller rows. - Corrected search visibility in Clips when disabled. - Fixed missing key-value pairs in the request for GAM Ads. - Ensured Clip events are sent correctly when opened from a category or search screen. ## **10.7.0** - 10/12/24 ### Breaking Changes - Added `customScreenForCategory()` to `StorytellerDelegate` for handling custom followable category actions. [StorytellerDelegates.md](StorytellerDelegates.md) - Added `useCustomScreenForCategory()` to `Storyteller` for allowing the developer to conditionally show the custom screen for a specific category or default. [StorytellerDelegates.md](StorytellerDelegates.md) Note that if you are not using this feature you can simply return the default return values for these delegate methods. ### Bug Fixes - Fixed issue where pausing required multiple clicks. - Resolved a bug preventing tablet ads from rendering correctly. - Fixed crash occurring in stories. - Addressed issue where old content briefly displayed after following/unfollowing a team. - Fixed bug where the "Following" tab showed user content after logout. - Resolved issue with custom native and client ads not resetting progress correctly. - Fixed incorrect calculation of Poll & Quiz results. - Analytics events: + Fixed `nextClip` and `previousClip` event issues. + `finishedClip` event now includes correct clip information when dismissing a category. - Fixed issue where tapping the left side of the first story closed it. - Resolved crash when clicking on two clips simultaneously. - Removed live chip from the search page. - Fixed "Not found" message incorrectly appearing on the search page. - Actions for followable categories now work correctly. - Fixed top-level back button functionality. - Addressed issue where the refresh button briefly appeared for a loading clip. - Disabled the search button when the search field is empty. - Collection name inputs are now trimmed correctly. - Resolved issue where banner ads occasionally missed the Google "i" icon. - Fixed clip grid on the search page not scrolling to a dismissed clip. - Fixed GAM analytics inside the Storyteller Ads Module. - Fixed KVPs for Banner Ads inside the Storyteller Ads Module. ### New Features - Added `resumePlayer()` to `Storyteller` for resuming the player after pausing. [AdditionalMethods.md](AdditionalMethods.md) ### Improvements - Live clips now resume from their current live position when viewed again. - Story rows behavior aligned with disabled Read/Unread ordering. - Added fallback to Story ID when a page cannot be found. - Updated captions to include a CC logo with a toggle option. - Set default font sizes for Stories, Clips, and Singletons in the Storyteller Home theme. - Clips now pause automatically on the clip player when an action is triggered. - Improved caching logic for Stories and Clips. ## **10.6.5** - 22/11/24 ### Bug Fixes - fix deprecated `Modifier.animateItemPlacement` and replaced with `Modifier.animateItem` ## **10.6.4** - 1/11/24 ### Bug Fixes - fix for sound playing on double back click on followable categories ## **10.6.3** - 25/10/24 ### New Features - add `isLive` and `collectionClipCount` fields to analytic events, see [Analytics](Analytics.md) for more details ### Bug Fixes - fixed an issue with Native Ad impressions not being reported in StorytellerGamModule - fixed a crash which could potentially happen when using hardware keyboard in Search screen ## **10.6.2** - 22/10/24 ### New Features - new signature for `StorytellerGamModule.init` method, which allows using different Ad Units, see [Ads](Ads.md) for more details ### Bug Fixes - fixed issue with potential crash when resuming CLips player - fixed issue which could cause ANR when launching the App using cold start ## **10.6.1** - 10/10/24 ### Bug Fixes - fixed multiple issues with playback stuck on For You / Following - fixed an action button not working for the empty Following screen ## **10.6.0** - 8/10/24 ### Breaking Changes - Compose Bom dependency updated to `2024.09.03` ### New Features - Added For You / Following feature to our embedded clips - Added auto reloading support to our lists ### Improvements - Some of our theme parameters can be configured from CMS - Updated our theme parameters documentation - Fixed standard banner ads not displaying on Tablets - Fixed a crash due to reorder animation issues - Video playback improvements - Video and Image preloading and caching improvements - Added Swipe gesture to dismiss Category screen ### Bug Fixes - fixed a crash which could occur when doing PTR on Embedded Clips - fixed a UI glitch for Native ads - fixed a bug where the Search screen was reloaded after closing the Player - fixed UI glitches on Category screen - fixed `dismissedAd` being sent for Stories on the Search/Category screen ## **10.5.1** - 10/09/24 ### Improvements - Ads will restart the video if the last action was `play` or `idle` - Introduced new tracking of ads when they are looped/repeated or Action buttons are tapped. - All add type events now include advertiser name ### Bug Fixes - fixed wrong use of googleAdsResponseId in ad tracking - fixed an issue where the incorrect cursor color was used in the search bar - fixed an issue where page ad events are also triggered when swiping down - `pageActionType` is now included for all types of ads and their analytic events - story ads analytics are correctly referred to as `video` instead of `image` - fix incorrectly laid out banner ads ## **10.5.0** - 3/09/24 ### New Features - added support for Standard ads GAM `Google Ads Manager` - introduced Storyteller Gam module `StorytellerGamModule`. It can be used to facilitate the integration of native ads into the Storyteller SDK so that the adverts are injected between the stories and clips. see [Ads](Ads.md) for more details - added `Storyteller.module` list during initialization to enable the native ads feature. see [StorytellerModule](StorytellerModule.md) for more details ### Improvements - added support for keyboard navigation for categories in the Clips player - buttons have been added to expand and shrink both the title and category lists - improved the animation of closing the player so it's less noticeable - improved keyboard focus handling within Search - aligned the embedded Clips play button to match the normal Clips play button - improved text input and keyboard responsiveness in search for Samsung devices - improved accessibility descriptions for the Clips player ### Bug Fixes - fixed a bug where it was possible to trigger requests to the API before the SDK was initialized - fixed a bug where navigation was possible to the same category in the category list - fixed a crash that could occur when opening stories from a story list - fixed a stack overflow crash for tablets when changing orientation - fixed broken layout for large titles and category lists on tablets and phones - fixed a bug where the live chip would be shown as read before it's clicked - fixed a bug where `No results found` would not show when tapping on the search button - fixed an issue where the gradient on an asset had a delay on reload ## **10.4.1** - 9/08/24 ### Bug fixes - fixed a crash that could occur when rendering a Row or Grid with animations enabled ## **10.4.0** - 8/08/24 ### New Features - added support for Accessibility adherence according to [WCAG 2.1](https://www.w3.org/TR/WCAG21/) standards - added support for keyboard navigation in the Stories and Clips player ### Improvements - added talkback announcements when navigating between Stories and Pages - improved all accessibility elements for all elements in Stories - upgraded the `HTTP/1` protocol to `HTTP/2` for better performance and security - enabled the re-order animations by default in themes please see `theme.lists.animateTilesOnReorder` [Themes](Themes.md) for more details - improved the alignment for labels and headings between ios and android - improved accessibility bypass/skip blocks for the Stories and Clips player - added error state when search fails to load - reduced load of dependency graph by deferring some dependencies initialization - improved animations for Stories and Clips - improved preloading of category thumbnails inside Clips ### Bug fixes - fixed analytic discrepancy issues (double clicks being ignored or filtered out) - fixed various mislabeled accessibility elements (action buttons, titles, images, Polls and Quizzes) - fixed preloading of poll videos not cancelling - fixed android back button not interacting correctly - fixed infinite loading or no loading at all in search - fixed a bug for followable categories where you cannot follow or unfollow - fixed a bug where sound is playing in the background - fixed a bug where sound is not playing when you return to the app - fixed a bug where images would not load or preload in wrong order - fixed a crash related to android 8.0.0+ devices - fixed a bug where poll videos would not play after coming back to the app - fixed a bug where Dismissed Reason was missing in Story Closed event - fixed a bug where previous search results were shown after multiple queries - fixed a bug with excessive space between title and categories in the Clips player - fixed a bug where "New" chip on Clip tiles would appear and disappear when opening the Clips player - fixed a bug where Story player would freeze after putting app into background - fixed a bug where sound would not stop playing when going between image and video pages in Stories - fixed a bug where pull-to-refresh on embedded Clips could stop playback - fixed a bug where there would be a flash on search screen during orientation change - fixed a bug where Story row inside search screen would appear after a delay - fixed a bug where search input in dark mode still had light mode style - fixed a bug where followable category screen would miss a "star" icon ## **10.3.0** - 15/07/24 ### New Features - lists can now reload in an animated fashion, controlled by the [animateTilesOnReorder](Themes.md) flag in the Theme. - [Storyteller Home](StorytellerHome.md) can now have a fixed UI Style set ### Improvements - standardizing Clips and Stories Player present/dismiss transitions - improved loading sequence when the tiles are appearing on the screen to feel smoother - improving the Accessibility experience of the Player - made clarifications around how the [User ID](Users.md) is used within the SDK - organized the docs for the [Open Player](OpenPlayer.md) methods - improved loading time of the lists in Compose - improved Search and Categories screens in Compose ### Bug fixes - fixed issue with the Theme not applied when opening Search - fixed issue with Story Playback mode in Analytics - fixed issue with the title gradient not appearing in square tiles - fixed crashes related to opening Quizzes and Polls - fixed View Model crash when accessing from a detached fragment ## **10.2.3** - 5/07/24 ### Bugfixes - fixed a crash which could occur when try to open story from deep link, but story is not available ## **10.2.2** - 3/07/24 ### Bugfixes - fixed a crash which could occur when reloading the row with Quizzes ## **10.2.1** - 27/06/24 ### Improvements - when an empty String is passed for `UserInput` in `Storyteller.initialize(...)` the default user will be used ## **10.2.0** - 24/06/24 ### New Features - added new liveChip properties to `rectangularTile` the theme, see [Themes](Themes.md) for more details - added new fade loading effect for images in rows and grids when transitioning from placeholder to the actual image - added support for linking to a specific clip within the moments tab including correct analytic events surrounding this - exposed the property `canGoBack` from embedded clips to allow querying about the possibility of going back in the embedded clip collection, see [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details - added support for initial Category within the `openCollection` method, see [Additional Methods](AdditionalMethods.md) for more details - added support for initial category for the deep links. Via the query param `categoryId` you can specify the initial category to open, see [DeepLinking](DeepLinking.md) for more details - added support for initial category for embedded clips. see [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details ### Breaking Changes - renamed `theme.player.liveChipImage` to `theme.player.liveChip.*` that contains `textColor`, `image`, `backgroundGradient`, and `backgroundColor` properties for better customization, see [Themes](Themes.md) for more details - removed `theme.tiles.rectangularTile/circularTile.liveChip.textColor` in favor of `theme.tiles.rectangularTile/circularTile.liveChip.readTextColor` and `theme.tiles.rectangularTile/circularTile.liveChip.unreadTextColor`, see [Themes](Themes.md) for more details ### Bugfixes - fixed an issue where backButton was not visible when navigating from a category to home then back to Moments - fixed an issue with multiple filter menus appearing when fast tapping on search filter button - fixed an issue where where action buttons shift incorrectly on Moments screen/tab - fixed various alignment issues & animations & theming issues across the SDK (Followable categories, Moments, etc.) - fixed a bug where the after killing the app and reopening, the theme resets - fixed playback issues while navigating - fixed analytics issues where nextCategory and previousCategory is sent instead of openedCategory and dismissedCategory - fixed bug where stories would close when navigating and trying to show an advert - fixed playback gets stuck when navigating & skipping too fast - fixed a lifecycle bug where sound plays when app is swiped to background - fixed a crash when swiping fast between moments and pressing home immediately afterwards - fixed an issue with search filters on tablets, filters sheet was half visible - fixed an issue with first party ads deep-links were not working - fixed a potential crash with first party ads - fixed stuck playback when opening a page ad - fixed a glitch with embedded clips header shifting on tablet layouts (samsung devices) when opening the share dialog - fixed an issue with square tiles that made title gradient flash when navigating between screens ## **10.1.5** - 10/06/24 ### Bugfixes - fixed an issue where the round tile title can be cut off in Compose ## **10.1.4** - 5/06/24 ### Bugfixes - fixed an issue with NPE crash which could happen when dismissing a Clip before video loads in `StorytellerClipsFragment` ## **10.1.3** - 5/06/24 ### Bugfixes - fixed an issue with NPE crash which could happen when opening the Stories Player - fixed an issue with ANR which could happen when initializing the SDK - fixed an issue with Stories crashing due to `ConcurrentModificationException` ## **10.1.2** - 23/05/24 ### Bugfixes - fixed an issue with Stories crashing due to `ConcurrentModificationException` ## **10.1.1** - 17/05/24 ### Bugfixes - fixed an issue with playback not starting for StorytellerClipsFragment - fixed an issue with the StorytellerClipsFragment playback not starting after reloading the container screen ## **10.1.0** - 16/05/24 ### New Features - added support for First Party Ads - added support for gradients for Circular Tiles, see [Themes](Themes.md) for more details ### Improvements - improved content preloading for faster playback ### Bugfixes - fixed an issue with the Row blinking on Tablets - fixed an issue with `PreviousClip` analytics event - fixed an issue where `CompletedStory` analytics event was not sent for single Paged Stories - fixing an issue with the filters on Search screen - fixing an issue with wrong theme applied when navigating between categories in `StorytellerClipsFragment` - fixing an issue where the long version of the title was used instead of the short one for the Circular tiles - fixing an issue where multiple Page Ads could be added in the same slot - fixing an issue where sometimes No Results Screen was not shown correctly in Search - fixing an issue where Clips could sent an error to delegate while reloading the content ## **10.0.3** - 27/05/24 ### Bugfixes - fixed an issue with Stories crashing due to `ConcurrentModificationException` (backport from **10.1.2**) ## **10.0.2** - 09/05/24 ### New Features - added compose implementation for embedded clips the `StorytellerEmbeddedClips` composable, see [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details ### Bugfixes - fix an issue for stories where the player is dismissed when the user navigates to search from stories - fix an issue where grid columns could be mixed when recycled ## **10.0.1** - 03/05/24 ### Breaking Changes - `reloadData()` for embedded clips will take you back one level if not on the top level of the collection. se [Reload Data](StorytellerClipsFragment.md#reload-data) in [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details - fixed an issue with `tileSpacing` where it was applied twice, so make sure to adjust your theme if you were using 2x `tileSpacing` value ### New Features - introduced `setLocale` method to the `Storyteller.user` object to set the user's locale, see [User Customization](Users.md) for more details ### Bugfixes - fixing an issue with clips subtitles can disappear when tapping the share button - fixing the missing search-specific analytics parameters for Stories - fixing an issue with the tile shrinking animation when the player is closed - fixing an issue where action buttons were not working on subsequent screens - fixing an issue with action button could crash the app when tapped after opening category details screen - fixing an issue where clips playback could be stuck after opening a story from category details screen - fixing an issue with embedded clips where swiping left to open category details might not work while on the first clip - fixing an issue with embedded clips reloading, back arrow can be still visible when it should not ## **10.0.0** - 18/04/24 ### Breaking Changes - deprecated xml view params removed in favour of `StorytellerListConfiguration`, see [StorytellerStoriesView](StorytellerListViews.md) or [StorytellerClipsView](StorytellerListViews.md) for more details - added an additional method `categoryFollowActionTaken` to `StorytellerDelegate`, see [StorytellerDelegate](StorytellerDelegates.md) for more details - `isRefreshing` parameter for Composable Lists has been replaced with `state.reloadData()` method, see [StorytellerCompose](StorytellerListViews.md) for more details ### New Features - introduced **Followable Categories**, enhancing user interaction and engagement by allowing users to follow/unfollow categories. For more details, please see [User Customization](Users.md) - `StorytellerListViews` now make use of ComposableViews which improve performance, see [StorytellerStoriesView](StorytellerListViews.md) or [StorytellerClipsView](StorytellerListViews.md) for more details - introduced Captions support for Clips - various Performance improvements for Lists scrolling and Tile loading ### Bugfixes - fixing some UI issues when navigating categories inside Clips - fixing issue with scrolling to the top of the list after reloading data - increased rounded corners default Theme value to 8 - fixing data reloading and scrolling for Composable Lists - added missing empty state for Search - fixed sound playing before navigation screen opens - fixed issue with previous play card visible when doing PTR on Clips - fixing analytic events and data for Clips, Search, Ads and Stories ## **9.13.4** - 10/04/24 ### Bugfixes - fixing a bug where `Storyteller.dismissPlayer` was ignoring `animate` parameter ## **9.13.3** - 8/04/24 ### Bugfixes - removed unused flag from manifest ## **9.13.2** - 2/04/24 ### Bugfixes - added R8 rule for `StorytellerHomeFragment` to prevent obfuscation ## **9.13.1** - 2/04/24 ### Bugfixes - aligned the `cornerRadius` theme property for the Stories action buttons and Onboarding ## **9.13.0** - 29/03/24 ### New Features - added `StorytellerHome` support, see [StorytellerHome](StorytellerHome.md) for more details - added better support for configuring privacy and analytics events, see [Additional Methods](AdditionalMethods.md) for more details ### Bugfixes - fixed an issue where Clips were started with a small delay - fixed an issue where Stories action buttons were not respecting `cornerRadius` theme property - fixed an issue in Compose where background was not properly resized for Live/Read/Unread chips ## **9.12.1** - 27/03/24 ### Bugfixes - fixed an issue where `openedReason` could be wrong when opening Clips/Stories player via a deep link - fixed an issue where the `shareSuccess` analytics event was not sent when sharing a Clip - fixing an issue where `Storyteller.openStory` was not emitting `openedStory` - fixing a black screen issue when opening not existing Clip collection ## **9.12.0** - 18/03/24 ### Improvements - added `itemTitle` and `actionText` to action analytic events, see [Analytics](Analytics.md) for more details - changed `Storyteller.currentUser` to be `Storyteller.currentUserId` - gradient mask increased on the tiles for better readability ### Bugfixes - fixed a bug where the wrong link was used for email sharing - fixed a bug where it was not possible to correctly alignment for Compose tiles - fixed handling of custom live chip in Compose - fixed an issue where it was possible to get `onFailure` callback when `onSuccess` was already triggered for `Storyteller.initialize` - fixed handling of the custom theme images in Compose ## **9.11.0** - 8/03/24 ### Improvements - added support for handling share on action buttons ## **9.10.0** - 28/02/24 ### Improvements - analytics improvements: added missing fields, fixed some events ### Bugfixes - fixed a bug with the `placement` field was set incorrectly for `AdRequestInfo` - fixed a bug where the search icon was visible when CLip Player loading failed - fixed and improved multiple areas of Analytics - fixed a bug where the image title was visible in Clips player on Ads - fixed a bug where filtering by content type was not working in Search - fixed a bug where the Search button was not working when using keyboard - fixed Compose list views where it was possible to see viewed/not viewed badges on Clips even when they were turned off in the CMS ## **9.9.2** - 21/02/24 ### Bugfixes - fixed a bug with the SHARE_SUCCESS event not having correct data and sometimes not being sent ## **9.9.1** - 02/02/24 ### Improvements - improved accessibility support for the player so that it announces player content without saying page 3x times ### Bugfixes - fixed a bug where Clips player was opening slowly after a delay and sometimes not opening at all - fixed a crash which could occur when opening the Clips player at a very edge case scenario ## **9.9.0** - 31/01/24 ### New Features - added 'Viewed/Not Viewed' status indicators for Clips, improving content tracking for users - removed support for round tiles in xml `StorytellerClipListViews`, streamlining the design - implemented sortOrder handling for Clips, offering better content organization - if `enableReadUnreadOrdering` is set to `false` for stories, New chips are now hidden for both read/unread statuses, ensuring a cleaner UI ### Bugfixes - fixed the order inconsistency between list and embedded Clips, enhancing the user experience - resolved the issue where Clips ordering was incorrect and ensured that Pull-to-Refresh (PTR) maintains the correct order - addressed the issue where the new chip did not disappear if the clip was watched on Search, ensuring consistent UI behavior - fixed the issue where like and share options were removed after reloading on embedded clips, maintaining feature availability - resolved the issue where, after a second PTR, read clips would become unread again, ensuring consistent read status - fixed the issue where the gray background remained static for a few frames for clips, polishing the visual experience ## **9.8.4** - 11/01/24 ### New Features - Embedded Clips has a new property `bottomInset` which can be used to set the bottom padding for the Clips Player, please see `Inset Management` section in [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details - Storyteller has a new property `isSearchEnabled` which can be used to check if the search is enabled for the current tenant, please see [Additional Methods](AdditionalMethods.md) for more details - added accessibility support for Embedded Clips, Search, Instructions and Compose ### Bugfixes - fixed an issue with round tiles layout in Compose - fixed an issue with `StorytellerListViewDelegate` methods are sometimes not triggered - fixed an issue to remember start clip id even if reload reorders clip data in embedded clip ### Breaking Changes - Storyteller Themes are accepting now resource ids for Fonts and Drawables using helper functions, see [Themes](Themes.md) for more details ## **9.8.3** - 13/12/23 ### New Features - added support for `StorytellerClipsViewControllerDelegate` inside `StorytellerClipsFragment`, please see [StorytellerClipsFragment](StorytellerClipsFragment.md) ### Improvements - improved Round tiles in Compose - improved PTR support for `StorytellerClipsFragment` ### Bugfixes - fixing an issue with not working `StorytellerDelegate` when the hosting activity is closed - fixing an issue with `CompletedStory` not being sent when the user is on the last page ## **9.8.2** - 1/12/23 ### Bugfixes - fixing an issue with Compose round tile titles ## **9.8.1** - 24/11/23 ### Bugfixes - fixing read/unread Stories order in Player - the top bar in Clip Player is now fixed - fixing default height constraints for Compose rows - fixing deep link handling for Stores/Clips when ids that are passed are wrong ### Breaking Changes - Storyteller Themes are accepting now resource ids for Fonts and Drawables only, see [Themes](Themes.md) for more details ## **9.8.0** - 9/11/23 ### New Features - rewritten `StorytellerListViews` fully in Jetpack Compose, please see [StorytellerCompose](StorytellerListViews.md) for more details - Clip collections are not looping on `StorytellerClipsFragment` anymore (user cannot advance from last to first Clip), please see [StorytellerClipsFragment](StorytellerClipsFragment.md) ### Breaking Changes - Compose versions of `StorytellerListViews` do not use controllers anymore, please see [StorytellerCompose](StorytellerListViews.md) for more details - updated Compose BoM dependencies to `2023.10.01` ## **9.7.7** - 29/11/23 ### Bugfixes - fixing a crash which could occur when dismissing Clips player ## **9.7.6** - 14/11/23 ### Bugfixes - fixing a crash which could occur when opening Clips or Stories player ## **9.7.5** - 9/11/23 ### Bugfixes - fixing a crash which could occur when using PTR on the Embedded Clips ## **9.7.4** - 8/11/23 ### Bugfixes - fixing an issue with `displayLimit` when same category is used in multiple places - fixing an issue with wrong count format of Clip Likes inside the player - fixing an issue with Stories/Clips Player close animation ### New Features - added support for `reloadData()` and PTR for `StorytellerClipsFragment`, please see [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details ## **9.7.3** - 18/10/23 ### Bugfixes - fixing a crash which could occur when navigating to the Embedded Clips via a deep link ## **9.7.2** - 16/10/23 ### New Features - Storyteller now supports Android 14 - upgraded Android Gradle Plugin from `8.0.2` to `8.1.1` ## **9.7.1** - 9/10/23 ### Bugfixes - fixing an action button issue for Clips ### New Features - added top level back button handling for Embedded Clips, please see [StorytellerClipsFragment](StorytellerClipsFragment.md) for more details ## **9.7.0** - 29/09/23 ### Bugfixes - fixing an issue with the Clip Player header image - fixing an issue where th instructions screen was not shown when changing the orientation - fixing memory leaks in Story Player ### New Features - added `openCategory` method, please see [Additional Methods](AdditionalMethods.md) for more details - added `openStoryByExternalId` and `openClipByExternalId` methods, please see [Additional Methods](AdditionalMethods.md) for more details - added `pageTitle` parameter to analytic events, please see [Analytics](Analytics.md) for more details - added fixed order rows mode for Stories which can be set up via the CMS - added pinned stories support for Stories which can be set up via the CMS ## **9.6.0** - 18/09/23 ### Improvements - updated top and bottom shade gradient for Clips - more/less button displayed below the categories list for Clips - improved sorting of Stories lists ### Bugfixes - fixing a bug where the Story close animation had a low quality - fixing memory leaks in the Clips/Stories player ## **9.5.0** - 30/08/23 ### New Features - added support for Page Ads which appear between Pages ### Breaking Changes - changed signature of ad delegate from `getAdsForList` to `getAd`, please see [Ads](Ads.md) for more details - changed classes inside `StorytellerAdRequestInfo`, `StoryInfo` and `ClipInfo` are now single class `IntemInfo`, please see [Ads](Ads.md) for more details ### Improvements - added `adPlacement` to analytics, please see [Analytics](Analytics.md) for more details ## **Important** - **Please note that the Storyteller SDK is now distributed via CloudRepo. Please see [GettingStarted](GettingStarted.md) for more details. MyGet repository is not working anymore** - The only change required is to add the following maven repository to your project's `build.gradle` file and MyGet can be removed: ```groovy maven { url 'https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk' } ``` ## **9.4.0** - 15/08/23 ### New Features - added support for Clips Category Navigation to the Clips Player ## **9.3.1** - 1/08/23 ### Improvements - improved performance of loading Story tile images ## **9.3.0** - 24/07/23 ### Breaking Changes - Composable views are part of the main SDK. Please refer to the [StorytellerCompose](StorytellerListViews.md) documentation for more details. - instead of setting params on Android views (which are now marked as deprecated), `StorytellerListViewConfiguration` object was introduced. Please see [StorytellerClipsView](StorytellerListViews.md) and [StorytellerStoriesView](StorytellerListViews.md) for more details. - Composable views are now also using `StorytellerListViewConfiguration` object. Please refer to the [StorytellerCompose](StorytellerListViews.md) documentation for more details. - some class names related to Ads were changed: | Old class | New class | | ---------------------------------------------------- | ------------------------------------------------------------- | | `ClientAd` | `StorytellerAd` | | `TrackingPixelClientAd` | `StorytellerAdTrackingPixel` | | `ClientAction` | `StorytellerAdAction` | ### Improvements - better scroll position handling for the Row views - improvements for Composable view performance ## **9.2.2** - 4/07/23 ### Improvements - added better support for Video and Image pages between Quiz pages ## **9.2.1** - 19/06/23 ### Bug fixes - fixed an issue with sharing Clips ## **9.2.0** - 13/06/23 ### New Features - added support for embedding the Clips Player as a fragment, see [StorytellerClipsFragment](StorytellerClipsFragment.md) ### Improvements - changed underlying image loading library to Coil to improve image loading performance - improved open/close transition animations - removed `StoryCategory` and `ClipCategory` classes in favour of `Category` class ### Bug fixes - fixed a problem with init spikes in the storyteller initialisation process - fixed a problem with a previous story thumbnail being visible when navigating stories quickly - fixed a problem with updating the read status of stories - fixed a problem with closing the instructions screen by tapping anywhere on the screen ## **9.1.7** - 6/06/23 ### Bugfixes - fixed a crash which could appear when inflating the views - fixed a return animation issue from the tiles ## **9.1.6** - 23/05/23 ### Bugfixes - fixed a bug where ads are not cleared when the Player is dismissed ## **9.1.5** - 18/05/23 ### New Features - `Storyteller.user.setCustomAttribute` now accepts only `String` values, see [User Customization](Users.md) ### Bugfixes - fixed a bug where the Page action could be triggered by a swipe up even if it had no action set ## **9.1.4** - 16/05/23 ### New Features - Ad indicator added to the Clips player ## **9.1.3** - 11/05/23 ### New Features - `adType` param added to `UserActivity`, see [Analytics](Analytics.md) - changed definition of `ClipsAdRequestInfo`, see [Ads](Ads.md) ## **9.1.2** - 04/05/23 ### Bugfixes - fixed a bug with dependency injection that might affect unit tests ## **9.1.1** - 02/05/23 ### Bugfixes - fixed a bug with proguard rules for AGP 8.0 ## **9.1.0** - 01/05/23 ### New Features - live clips are here. - added Live indicator to the Clips tile and the Clips player - implemented during live stream behaviour - implemented before/after live stream behaviour ### Bugfixes - fixed pager scrolling glitch on clips when having two clips - fixed gray button appearing on Quiz summary when share button is turned off in theme - fixed a potential crash which could occur when sending Storyteller to the background ## **9.0.1** - 17/04/23 Improvements: - added support for Stories ordering by category Bugfixes: - fixing an issue with unit tests - fixing an issue with the Clips title image not appearing - fixing a crash which could occur when navigating the Stories inside a grid - fixing a a crash which could occur when navigating to a previous story ## **9.0.0** - 6/04/23 Improvements: - added support for Ads between Clips - performance improvements for Clips Player - added support for setting custom attributes for Storyteller requests - improved image preloading performance for Story and Clip tiles - removed deprecated `tileBecameVisible`, `getAdsForList` methods Bugfixes: - fixing a bug where the Story tiles were not loaded immediately when scrolling `StorytellerListView` - fixing a bug where `StorytellerListView` was not scrolled to the beginning after reloading the data Breaking Changes: - removed support for `category` on Clips and `collection` for Stories - changed `getAdsForList` callback to better support Clips and Story Ads, see [Ads](Ads.md) - introduced a better class hierarchy for the StorytellerListView classes: ```text StorytellerListView ├── StorytellerStoriesView │ ├── StorytellerStoriesRowView │ └── StorytellerStoriesGridView └── StorytellerClipsView ├── StorytellerClipsRowView └── StorytellerClipsGridView ``` for more information please check the [StorytellerListView](StorytellerListViews.md) documentation. ## **8.5.11** - 12/12/23 Bug Fixes: - fixing an issue with not working `StorytellerDelegate` when the hosting activity is closed ## **8.5.9** - 3/04/23 Bug Fixes: - fixing a rare crash which could occur when the player is recreated ## **8.5.8** - 27/03/23 Improvements: - introduced different thumbnail sizes for better story and clip previews ## **8.5.7** - 14/03/23 Bug Fixes: - fixed a bug where a Video analytic event was not sent on time ## **8.5.6** - 2/03/23 Bug Fixes: - fixed a bug with gestures which could appear when moving the app to the background and pressing on a Story. - fixed a bug where the video would not start after opening a Story from a deep link - fixed a crash that could occur when there is a large number of tiles in the `StorytellerListView` ## **8.5.5** - 20/02/23 Bug Fixes: - fixed a bug that was causing the onboarding process to appear every time the application was initialized - fixed a bug where the ads created by the GAM were not properly animating upon closing ## **8.5.4** - 15/02/23 Bug Fixes: - fixed a bug where category parameters were not being set for Story, Clip and Ad analytics events - fixed a bug that was causing the ad chip to not be displayed for Client ads ## **8.5.3** - 10/02/23 Bug Fixes: - fixed a bug with setting an empty user id (`null`) during Storyteller initialization - fixed a bug with a video playing under the instructions screen - fixed a bug with the Stories player layout - fixed a bug with the opening transition ## **8.5.2** - 2/02/23 Bug Fixes: - fixed a bug with wrong `openedReason` set for Clips analytics ## **8.5.1** - 30/01/23 Bug Fixes: - fixed a crash which could occur on Android versions lower than 10 Improvements: - improvements to the close transition handling ## **8.5.0** - 19/01/23 Breaking changes: SDK Repackaging was done to organise the code better. Please update Storyteller imports. | Old class | New class | | ---------------------------------------------------- | ------------------------------------------------------------- | | `com.storyteller.domain.StorytellerListViewCellType` | `com.storyteller.domain.entities.StorytellerListViewCellType` | | `com.storyteller.services.Error` | `com.storyteller.domain.entities.Error` | | `com.storyteller.domain.AdResponse` | `com.storyteller.domain.entities.ads.AdResponse` | | `com.storyteller.domain.ClientStory` | `com.storyteller.domain.entities.ads.ClientStory` | | `com.storyteller.domain.ListDescriptor` | `com.storyteller.domain.entities.ads.ListDescriptor` | | `com.storyteller.domain.UserActivity` | `com.storyteller.domain.entities.UserActivity` | | `com.storyteller.domain.UserActivityData` | `com.storyteller.domain.entities.UserActivityData` | | `com.storyteller.domain.UserInput` | `com.storyteller.domain.entities.UserInput` | | `com.storyteller.domain.ClientAd` | `com.storyteller.domain.entity.ads.ClientAd` | Bug Fixes: - fixed a bug with `onPlayerDismissed` callback - fixed a bug with `onDataLoadComplete` callback - fixed a bug with Video from previous story playing in background - fixed bugs with some parameters not set for the analytic events Improvements: - added `topInset` and `bottomInset` to grids - - please see [Themes](Themes.md) for more details - added better support for open/close transitions using `activityReentered` method ## **8.4.0** - 16/12/22 Bug Fixes: - fixed a bug with the loading spinner on Clips - fixed rare bug with Story transitions - fixed a bug that causes Quiz Pages to be answered when tapping back - fixed an animation bug with Story tiles Improvements: - improvements to the prefetching of Clips' playcard image - implemented a more efficient UserActivity reporting system - corrected the use case of thumbnails and playcards - implemented single ExoPlayer to improve video playback ## **8.3.4** - 21/12/22 Bug Fixes: - fixed a bug with `dismissPlayer` on Clips ## **8.3.3** - 21/12/22 Bug Fixes: - added Clip Collection and Clip Categories event data for all Clip analytic events ## **8.3.2** - 21/12/22 Bug Fixes: - Skipped Ad events are now recorded when tapping on an ad and swiping left on an ad Improvements: - added Current Category to the event data for Story and Ad analytic events ## **8.3.1** - 15/12/22 Improvements: - added support for multiple font weights - replaced SHARE_TAPPED event with SHARE_BUTTON_TAPPED for event consistency between Stories and Clips ## **8.3.0** - 06/12/22 Bug Fixes: - fixed bug when a user taps outside a Story border on Quizzes - fixed default value for `answeredMessageTextColor` - fixed bug with presenting rounded corners on Stories - fixed bug with tracking pixels Improvements: - added additional properties to ad events to help identify individual ads - improvements to the positioning of Action buttons - modified the `getAdsForList` callback to provide more ad information - adjusted the timestamp property so timescales are updated for each new Page ## **8.2.9** - 29/11/22 Bug Fixes: - implemented a solution for a shared animation bug on Android 5 to Android 9 when targeting `singleInstance` activity Improvements: - stability improvements ## **8.2.8** - 25/11/22 Bug Fixes: - fixed bug with Live Chip properties - fixed bug with `onCurrentVideoPlayerChanged` event - fixed bug that could cause a `NullPointerException` - fixed bug that could cause a Story to not be marked as read when quickly switching Pages ## **8.2.7** - 22/11/22 Bug Fixes: - fixed bug with Stories not opening in Single Story mode when using deep links - fixed bug with user activity reports with Stories launched via deep link - fixed bug that causes delegates to be disconnected in certain instances - fixed bug with opening Clips whilst reloading data ## **8.2.6** - 15/11/22 Bug Fixes: - fixed rare crash when opening player ## **8.2.5** - 11/11/22 Bug Fixes: - fixed bug with Story playback when tapping quickly through Stories - fixed bug that could cause rare adapter crash - fixed bug with video playback when changing user Deprecations and Changes: - ads are now requested after every Story to align ad call logic with iOS SDK ## **8.2.4** - 04/11/22 Bug Fixes: - fixed bug that could cause the same Story to play after ad insertion - fixed bug that could cause rare crash after opening webpage links - exposed classes needed to support React Native ## **8.2.3** - 01/11/22 Bug Fixes: - fixed bug that could cause crash when changing Page whilst the player is being closed - fixed bug that could cause tracking events to be started incorrectly Improvements: - reduced memory footprint to improve playback - stopped empty or failed items appearing in StorytellerHomeView on rare occasions - implemented improved retry policy Deprecations and Changes: - removed all open methods from StorytellerListViews to just leave static methods ## **8.2.2** - 25/10/22 Bug Fixes: - fixed rare crash when opening player Improvements: - improved logging ## **8.2.1** - 21/10/22 New Features: - implemented preloading for Stories and Clips - added support for selective Story caching for Story rows Bug Fixes: - fixed bug with the opening animations of Trivia Quizzes - fixed bug with fetching ads after using airplane mode - fixed bug with video playback getting stuck - fixed bug with view model that could cause Story crashes Improvements: - improved analytics handling - improvements in handling of DeepLinkManager - improvements in data use cases ## **8.2.0** - 11/10/22 New Features: - added support for Clips and Collections Bug Fixes: - fixed bug that delays dismissing of an opened Story - fixed inconsistencies with `theme.engagementUnits.poll.answeredMessageTextColor` - fixed bug with `theme.player.liveChipImage` and `theme.primitives.cornerRadius` on Poll answers and Story Action buttons - fixed bug that could cause Story to freeze when tapping fast - fixed bugs with fresh tenants swiping up on an ad and closing ads - fixed swipe down animation bug - fixed bug with preview transitions on tablets - fixed bug that could cause user to move to a Story row instead of a Story when closing web Improvements: - added refresh button to Story videos - new and improved mechanism for batch ad handing, audio manager focus handling, events and callbacks - improved handling of media sharing - new and improved mechanisms for analytics and opening Story/Page IDs - performance improvements for Stories and Clips loading times - adding theme `theme.tiles.rectangularTile.unreadIndicator.textSize` - making all tile themes respect UiStyle for all tile types - open/close shared transition handling improvements ## **8.1.15** - 26/10/22 Bug Fixes: - fixed a bug where it wasn't possible to select files inside the Action Web View New Features: - added support for External Action Links ## **8.1.14** - 17/10/22 New Features: - added the ability to open a clip or collection directly using the `openCollection` method ## **8.1.13** - 21/09/22 Bug Fixes: - fixed option to save Story assets from a Story not displaying correctly ## **8.1.12** - 01/09/22 Bug Fixes: - fixed Story sharing issue between iOS and Android - fixed Quiz sharing issues ## **8.1.11** - 24/08/22 Bug Fixes: - fixed occasional crash when closing player - fixed reload button visibility not working Improvements: - renamed `Storyteller.isPresentingStory` to `Storyteller.isPlayerVisible` UI Changes: - improving the Story Player UI on tablet ## **8.1.10** - 19/08/22 Bug Fixes: - fixed issue of duplicate apps when share sheet opens - fixed issue where a Page with a Poll plays audio from the previous Page after sharing - fixed issue of Poll sharing only working if the Poll has been answered and shared from another Android device - fixed issue of video playback not playing on Video Poll after watching a Video Page - fixed issue where the `SHARE_SUCCESS` event was not sent Improvements: - improving the Story Player UI - improving the share sheet UI Changes: - navigation buttons added back into Stories - status bar visible in Stories ## **8.1.9** - 12/08/22 Bug Fixes: - fixed an issue where the share button could be cropped on smaller devices - fixed issue of grid being cropped on navigation to more Stories - fixed an issue where a placeholder could be briefly visible after close animation is finished Improvements: - improved audio focus handling on Android API < 26 (Oreo) New Features: - added `contentLength` parameter for analytics events - please see [Analytics](Analytics.md) for more details UI Changes: - general UI refinements for Story Tiles and Story Player - ensured all grid tiles have corner radii ## **8.1.8** - 11/08/22 Bug Fixes: - fixing some minor issues with the tile layouts Improvements: - improving the audio focus handling when the Story Player is open ## **8.1.7** - 05/08/22 Improvements: - update client ads `getAdsForList` public api - improve polls UI ## **8.1.6** - 02/08/22 New Features: - add support for circular tile border theming - add support for circular tile custom live chip theming ## **8.1.5** - 02/08/22 Bug Fixes: - fix a crash caused by triggering web action button - improve story player UI - fix row/grid `displayLimit` not working for placeholders ## **8.1.4** - 27/07/22 Bug Fixes: - fix row/grid placeholders styling for light/dark modes Improvements: - improve light/dark mode theme compatibility ## **8.1.3** - 25/07/22 New Features: - add support for multiple row/grids with the same categories ## **8.1.2** - 25/07/22 Bug Fixes: - fix bug where sometimes no Stories could be shown when categories are set Improvements: - improve performance or rows/grids inside recycler views ## **8.1.1** - 22/07/22 Bug Fixes: - fixing a rare crash which could occur when tapping on a web action button - fixing an issue where the share button for Polls was misaligned UI Changes: - improving the layout of the round Story tile items Deprecations and Changes: - removing `topInset` and `bottomInset` from grids ## **8.1.0** - 13/07/22 Improvements: - improving the video playback experience ## **8.0.3** - 11/07/22 Bug Fixes: - fix bug where live chip was incorrect for read/unread story tiles - fix inconsistent round tile sizes for varying lines of title text - fix title text alignment in round tiles ## **8.0.2** - 28/06/22 Bug Fixes: - fixing an issue where it was possible for a progress indicator to stay on a Poll page - fixing an issue where the live chip was not aligned correctly on a tile - fixing an issue with `lists.grid.columns` in theme ## **8.0.1** - 24/06/22 Bug Fixes: - fixing an issue with R8 and the SDK package name - fixing an issue where it was possible for video playback to not start when viewing a video Page - fixing an issue where it was possible for Stories not not be marked as Read - fixing an issue where it was possible for video playback to start in the background - fixing an issue where it was possible for video playback to freeze unexpectedly - fixing an issue where it was possible for the instructions screen to layout incorrectly - fixing an issue where it was possible for the share button to not be clickable in certain scenarios Improvements: - improving the performance of the Story Player New Features: - adding the ability to enable/disable Storyteller event tracking via the SDK method `enableEventTracking()` and `disableEventTracking` - please see [Additional Methods](AdditionalMethods.md) for more information - adding action buttons instead of swipe ups for Stories - adding `displayLimit` parameter to `StorytellerListView` - please see [Configuring a StorytellerListView](StorytellerListViews.md) UI Changes: - improving the UI for Polls and Quizzes Deprecations and Changes: - renamed callbacks in `StorytellerListViewDelegate` - please see [Implementing StorytellerListViewDelegate Callbacks](StorytellerDelegates.md) ## **7.1.0** - 29/04/22 Bug Fixes: - fixing an issue with the instructions screen UI New Features: - adding support for Live Stories - adding additional attributes to support Live Stories theming - please see [Custom Themes](Themes.md) for details UI Changes: - improving UI/UX experience in Story Player ## **7.0.3** - 12/04/22 Bug Fixes: - fixing a dependency clash with Koin library ## **7.0.2** - 0/04/22 Bug Fixes: - fixing an issue with ReactNative ## **7.0.1** - 04/04/22 Bug Fixes: - fixing an issues with quizzes and theme colors - fixing an issue where the `UiTheme` had obfuscated name ## **7.0.0** - 01/04/22 Bug Fixes: - fixing an issue where is was possible foe the new indicators to appear on placeholders - improving the category caching for faster load times - fixing a crash which could occur on initialization - fixing an issue where it was possible for the global delegate not to register - fixing an issue where it was possible for the share button to disappear after swiping up on a Story Improvements: - making it easier to define a custom theme - please see [Themes](Themes.md) for details New Features: - adding support for Trivia Quizzes ## **6.2.0** - 25/02/22 Improvements: - improved support for multiple `StoryListViews` inside a RecyclerView ## **6.1.4** - 14/03/22 Bug Fixes: - fixing an issue with the navigation for the video pages on Android 12 ## **6.1.3** - 08/03/22 Bug Fixes: - fixing a crash which could occur on the open/close story animation - fixing an issue with the `primaryTextColor` - fixing a crash which could occur when opening the web link ## **6.1.2** - 23/02/22 Bug Fixes: - fixing obfuscation issues ## **6.1.1** - 23/02/22 Bug Fixes: - fixing an issue with the reloading of the stories list - fixing an issue with the story tiles flickering when reloading Improvements: - improving the story tile resizing Deprecations and Changes: - `pagesViewedCount` is public again ## **6.1.0** - 16/02/22 Bug Fixes: - fixing an issue with navigation which could occur when ads are enabled - fixing an issue with poll vote calculations - fixing a bug where the story list tiles were not updated properly on older Android versions - fixing an issue with the colors from the theme not being applied properly for `StorytellerListViews` Deprecations and Changes: - `uiStyle` parameter moved to `StorytellerListView` (see here for more details [Configuring a StorytellerListView](StorytellerListViews.md) ## **6.0.2** - 07/02/22 Bug Fixes: - fixing obfuscation issues ## **6.0.1** - 03/03/22 Bug Fixes: - fixing an issue which could occur when opening a story when in the landscape orientation - fixing a crash which could occur on the open/close story animation Improvements: - improving the poll UI - improving the UI on the devices with notch ## **6.0.0** - 17/01/22 Improvements: - Improving the scaling behavior of story tiles - Improvements and bugfixes in how themes are handled New Features: - Added `StorytellerGridView` - Adding the ability to override the theme on a per list basis Deprecations and Changes: - Public API breaking changes: + package `com.storyteller.ui.row` renamed to `com.storyteller.ui.list` + `StorytellerRowViewDelegate` renamed to `StorytellerListViewDelegate` + `StorytellerDelegate`'s `getAdsForRow` renamed to `getAdsForList` + `StorytellerRowView`'s `insetRight` and `insetLeft` renamed correspondingly to `insetStart` and `insetEnd` + `Theme` class' property `row` renamed to `list` + `RowViewTheme` class renamed to `ListViewTheme` + `StorytellerRowViewCellType` renamed to `StorytellerListViewCellType` + `StorytellerRowViewStyle` renamed to `StorytellerListViewStyle` + `OpenedReason`'s enum value `STORY_ROW_TAPPED` renamed to `STORY_LIST_TAP`, tracking string `story_row_tapped` renamed to `story_list_tapped` + `StoryPlaybackMode`'s enum value `ROW` renamed to `LIST`, tracking string: `row` renamed to `list` ## **5.0.8** - 08/02/22 Bug Fixes: - fix for race conditions in case of rapid onPause/onResume scenarios ## **5.0.7** - 31/01/22 Bug Fixes: - fixing a potential issue which could occur when running unit tests with Storyteller ## **5.0.6** - 25/01/22 Bug Fixes: - fixing dependency issues ## **5.0.5** - 03/01/22 Bug Fixes: - fixing a concurrency issue ## **5.0.4** - 15/12/21 Bug Fixes: - fixing a crash which could occur when skipping through the pages fast ## **5.0.3** - 14/12/22 Bug Fixes: - fixing a crash which could occur when invoking `Storyteller.initialize` multiple times ## **5.0.2** - 02/12/21 Bug Fixes: - fixing a bug with the user activity data parsing Deprecations and Changes: - removing all `jcenter()` dependencies ## **5.0.1** - 24/11/21 Bug Fixes: - fixing a UI glitch where the initial story page could be shown when switching between the pages - fixing a crash which could happen when using delegates, `configureSwipeUpWebView(...)` is now mandatory to override - fixing a bug which could occur when using Storyteller ads ## **5.0.0** - 18/11/21 Improvements: - improving sending of analytics events New Features: - adding support for categories - adding support for opening the story player in single story mode without the `StorytellerRowView` - please see [AdditionalMethods](AdditionalMethods.md) for details - adding additional param `storyPlaybackMode` to analytics events - please see [Analytics](Analytics.md) for details Deprecations and Changes: - splitting `StorytellerListViewDelegate` into two separate delegates - please see [StorytellerDelegate](StorytellerDelegates.md) and [StorytellerListViewDelegate](StorytellerDelegates.md) for details - removing the `cellScale` parameter from the `StorytellerRowView` in favour of auto adapting to the parent's view height - removing callbacks from the `reloadData()` method ## **4.5.11** - 03/11/21 Bug Fixes: - fixing a crash which could occur when sharing the story on Android 12 ## **4.5.10** - 02/11/21 New Features: - adding additional method `configureSwipeUpWebView(webview: Webview, url : String?, favicon: Bitmap?)` to allow additional configuration of the WebView - please see [StorytellerListViewDelegate](StorytellerDelegates.md) ## **4.5.9** - 20/10/21 Bug Fixes: - fixing a crash which could occur when opening the story and scrolling the row in a container ## **4.5.8** - 13/10/21 Bug Fixes: - fixing a problem with ads and navigation - fixing a UI issue with the rounded corners on tablets ## **4.5.7** - 30/09/21 Bug Fixes: - fixing a potential clash with resource names - fixing a crash which could occur when using a malformed swipe up url ## **4.5.6** - 24/09/21 Bug Fixes: - fixing Windows build issue originating in the kotlinModule compilation artifact containing characters disallowed in windows filesystem ## **4.5.5** - 13/09/21 Bug Fixes: - fixing a bug where the swipe up to `in_app` links was not working New Features: - adding additional method `openDeepLink(String,Boolean,(Error) -> Unit)` to make Storyteller SDK integration easier - please see [AdditionalMethods](AdditionalMethods.md) and [Deep Linking](DeepLinking.md) - adding `secondaryColor` property to themes - please see [Themes](Themes.md) for details ## **4.5.4** - 31/08/21 Bug Fixes: - fixing a bug where the story could not be closed if the delegate was not set - fixing a bug where the poll percentages could show invalid values when the orientation is changed - fixing a rare crash which could occur when opening poll stories Improvements: - deep linking documentation update - please see [Deep Linking](DeepLinking.md) ## **4.5.3** - 11/08/21 Bug Fixes: - fixing a packaging issue ## **4.5.2** - 11/08/21 Bug Fixes: - fixing a bug where the poll answers could be cut on some devices New Features: - adding additional method `isStorytellerDeepLink(String)` to make Storyteller SDK integration easier - please see [AdditionalMethods](AdditionalMethods.md) ## **4.5.1** - 06/08/21 Bug Fixes: - fixing a tablet bug where the story player could be frozen when changing orientation ## **4.5.0** - 05/08/21 Improvements: - minor performance improvements ## **4.4.2** - 03/08/21 Bug Fixes: - fixing a bug where the `COMPLETED_STORY` event could be triggered too early Improvements: - improving the story tile loading states ## **4.4.1** - 28/07/21 Improvements: - improve in app swipe up handling New Features: - add `animated` parameter for `openPage` and `openStory` public methods ## **4.4.0** - 21/07/21 Bug Fixes: - fixing a bug which could occur when updating the story with new pages - fixing a UI issue with the instructions screen which could appear on some smaller tablets - fixing a bug where the story could be opened multiple times Improvements: - resources can be used to set the theme properties - please see [Themes](Themes.md) for details ## **4.3.3** - 06/07/21 Bug Fixes: - fixing a crash which could occur during the preload of images ## **4.3.2** - 02/07/21 Bug Fixes: - added another fix for crash caused by connection timeouts - fixing a bug where the swipe ups were sometimes not working ## **4.3.1** - 02/07/1 Bug Fixes: - added a fix for crash caused by connection timeouts ## **4.3.0** - 01/07/21 Bug Fixes: - fixing a crash which could occur when using custom ads - fixing a bug where the poll answers could start flickering Improvements: - improving the page loading states ## **4.2.0** - 10/06/21 Improvements: - internalizing the Exoplayer `2.14.0` to remove any version conflicts with the client applications New Features: - adding support for themes - please see [Themes](Themes.md) for details - adding additional methods to make Storyteller SDK integration easier - please see [AdditionalMethods](AdditionalMethods.md) ## **4.1.3** - 14/04/21 Bug Fixes: - fixed a bug where the poll video could be played after changing the page - fixed a crash which could occur when changing the orientation on tablets Improvements: - improved poll UI and animations ## **4.1.2** - 02/04/21 Bug Fixes: - fixed an issue with tile animation being clipped UI Changes: - extended the page duration when a user selects poll answer - improved poll UI and animations ## **4.1.1** - 24/03/21 Bug Fixes: - fixing a bug where the navigation on poll pages could stop working Improvements: - improving the network request format ## **4.1.0** - 19/03/21 Bug Fixes: - fixing a crash which could occur when sending analytics - fixing a bug where the instructions modal button could appear too low on tablets - fixing a bug where the story player could be closed when navigating through the stories which were read Improvements: - improving the poll UI ## **4.0.5** - 01/03/21 Bug Fixes: - fixing a crash which could occur when opening a story Improvements: - improving the animation when swiping between stories for Android 11 devices ## **4.0.4** - 15/02/21 Bug Fixes: - fixing a crash which could occur when opening a story from a deep link ## **4.0.3** - 11/02/21 Improvements: - improving the preloading of videos on poll pages - improving sharing to different apps ## **4.0.2** - 04/02/21 Bug Fixes: - fixing a crash which could occur when closing the story view - fixing a bug where changing the `rowScale` property on the Storyteller Row had no effect ## **4.0.1** - 01/12/21 Bug Fixes: - fixing some visual bugs which could occur on the poll pages - fixing a crash which could occur when closing the story view New Features: - adding support for video backgrounds on poll pages ## **4.0.0** - 25/01/21 Bug Fixes: - fixing a bug where it was possible for swipe ups to stop working - fixing a crash which could occur when saving analytics data Improvements: - performance improvements for video and image pages New Features: - adding support for poll pages - adding support for multiple instances of the Story Row in a single app Deprecations and Changes: - changing the method signatures of `openStory`, `openPage` and `reloadData` ## **3.17.5** - 15/12/20 Bug Fixes: - fixing a bug where the in-app browser address bar could be white in night mode ## **3.17.4** - 10/12/20 Bug Fixes: - fixing a bug where the read stories could be shown when tapping on unread story - fixing a bug where the profile image was not shown for ads - fixing a bug where the swipe up UI could be shown even if it was disabled - fixing a bug with the onboarding screen margins ## **3.17.2** - 10/12/20 Bug Fixes: - fixed a bug where an incorrect border color is applied to a story row item after it became read upon closing the story viewer ## **3.17.1** - 30/11/20 Bug Fixes: - fixing a crash which could occur when preloading videos for stories ## **3.17.0** - 27/11/20 Bug Fixes: - fixing a bug which could cause video playback to resume when a user was swiping to change stories - fixing a crash which could occur when preloading videos for ads - fixing a crash for some customers which could occur during swiping on Android 11 ## **3.16.4** - 18/11/20 Bug Fixes: - fixing a bug where the blank page was opened instead of a story ## **3.16.3** - 18/11/20 Bug Fixes: - fixing potential null pointer exceptions - fixing a bug where the Play Store was not opened, when swiping up on a page with redirect - fixing a bug where the `resumedAdPage` event was triggered too often - fixing a bug where the read status of a story was not saved ## **3.16.2** - 06/11/20 Bug Fixes: - fixing a bug introduced in 3.16.0 where it was possible for incorrect tracking pixels to be called for integrating app ads Improvements: - improving video preloading with the new version of Exoplayer Deprecations and Changes: - not sending extra parameters to tracking pixels supplied via integrating app ads ## **3.16.1** - 02/11/20 Bug Fixes: - fixing a crash which could occur when swiping to interact with the Story View ## **3.16.0** - 30/10/20 Bug Fixes: - fixing a crash which could occur when watching stories and changing from an image page to a video page - fixing the alignment of the dismiss icon within the story view relative to the story title - fixing a bug where if the system dark mode was changed when the story view was opened, the current story would persist behind other stories - fixing a bug where the new tag could not be removed from the story row tiles in certain circumstances Improvements: - improving the performance of the story view when quickly transitioning between pages New Features: - adding video playback events - please see [Analytics](Analytics.md) for details ## **3.15.5** - 06/11/20 Bug Fixes: - fixing a bug introduced in 3.15.3 where it was possible for incorrect tracking pixels to be called for integrating app ads ## **3.15.4** - 04/11/20 Bug Fixes: - fixing a crash which could occur when swiping to interact with the Story View ## **3.15.3** - 02/11/20 Bug Fixes: - fixing a crash which could occur when swiping to interact with the Story View ## **3.15.2** - 29/10/20 Bug Fixes: - fixing a crash which could occur when swiping to interact with the Story View - upgrading Exoplayer dependency to version 2.11.3 ## **3.15.1** - 22/10/20 Improvements: - improving the smartphone onboarding layout - improving the animation when story is opened - improving the preloading of videos Deprecations and Changes: - adjusting the initialize method errors - see [Getting Started](GettingStarted.md) for more details ## **3.15.0** - 16/10/20 Bug Fixes: - fixing a bug where rounded corners were not displayed correctly in the story view - fixing a bug on Android 8 where it was possible to see the story image as a background in the story view Improvements: - improving the preloading of stories in the story view - improving the video cache - improving the animation when a story is dismissed and returned to the row view - improving the sensitivity of the swipe up gesture ## **3.14.1** - 09/10/20 Bug Fixes: - fixing a bug where the instructions modal could be dismissed if the app was sent to the background when it was visible - fixing a bug where it was possible for tiles in the row to render incorrectly in certain situations Improvements: - improving the error handling around the `initialize` flow to reduce the likelihood of any errors when this method is called ## **3.14.0** - 02/10/20 Bug Fixes: - fixing a bug where it was possible for a user to be presented with a black screen with no loading indicator when opening the story view and on a slow connection - fixing a bug where it was possible for a loading indicator not to be presented when viewing an image page in certain circumstances over a slow/unreliable network Improvements: - improving the handling of retries when assets in the story view fail to load Deprecations and Changes: - adjusting the behavior of the instructions modal on tablets so that it appears over the story view rather than over the place in the app where it was presented from - cancelling requests to preload content for the story view if the user is on a slow network connection in order to maximize the chance they are able to view the content they have on screen ## **3.13.1** - 29/09/20 Bug Fixes: - fixing a bug where it was possible for tiles to appear blank in the story carousel - removing a memory leak from within the SDK - fixing a bug where if the `rowScale` for a row was changed while the row was being rendered, the story tiles could appear at the wrong size - fixing a bug which could cause the SDK to crash in certain circumstances - fixing a bug where it was possible to get stuck in the story view when skipping through a large number of stories and pages - fixing a bug where it was possible for the story view to get stuck whilst dismissing Improvements: - improving the behavior of the SDK with respect to transitions when a non-translucent status bar is used for the activity where the row view is displayed - improving the animation when a story is dismissed and returned to the row view ## **3.13.0** - 18/09/20 Bug Fixes: - fixing a memory leak within the SDK Improvements: - further improvements to the transitions between tiles in the story row and the story view New Features: - adding full and intentional tablet support to the SDK ## **3.12.0** - 04/09/20 Improvements: - improving email sharing by including the story title in any email sharing copy - improving the reliability of directing users to the Play Store from a swipe up action Deprecations and Changes: - exposing Story Categories on the `getAdsForRow` callback - please see the [Ads](Ads.md) documentation for more details ## **3.11.0** - 24/08/20 Bug Fixes: - fixing a bug where it was possible for an incorrect value to be passed for `openedReason` when a page completed - fixing a bug where it was possible for `completedStory` events not to fire when a user completed a story - fixing a bug where it was possible for an incorrect value to be passed to `dismissedReason` when a user completed the final page of the final story Improvements: - improving the open animation when a user taps a tile in the Story View New Features: - adding support for swiping up to a location within the integrating app or to the App Store from within Client Supplied Ads - see [Ads](Ads.md) for more details Deprecations and Changes: - adjusting the initialization flow of the SDK - see [Getting Started](GettingStarted.md) for more details ## **3.10.1** - 07/08/20 Bug Fixes: - fixing a bug where stories were not marked as read until `reloadData` was called - fixing a bug where it was possible for a story to be marked as unread when a page was removed from the story - fixing a crash which could occur when retrieving or persisting encrypted data - fixing a bug where it was possible for the Story View to open in landscape mode New Features: - adding a method to the [StorytellerListViewDelegate](StorytellerDelegates.md) called `tileBecameVisible` to allow the integrating app to be notified when a tile is visible on screen ## **3.9.0** - 31/07/20 Bug Fixes: - fixing a bug where text was misaligned when using square tiles - fixing a bug where it was possible to swipe twice on the final story and end up on a blank screen Improvements: - UI improvements to the Instructions Screen - improving the reliability of the activity tracking Deprecations and Changes: - adjusting the [StorytellerListViewDelegate](StorytellerDelegates.md) `getAdsForRow` callback to allow it to accept `null` values for `StorytellerAd` ## **3.8.3** - 27/07/20 New Features: - adding `dismissStoryView` method to `StorytellerRowView` to allow dismissing of any currently open Story View ## **3.8.0** - 15/07/20 Improvements: - ensuring the user's device doesn't sleep when watching stories - improving the experience when sharing via email UI Changes: - increasing the size of the tap target for the Swipe Up UI ## **3.7.4** - 14/07/20 Bug Fixes: - fixing a bug where it was possible for a blank screen to be displayed when pushing the device back button on the instructions screen - fixing a bug where it was possible for the story view to render at the size of a story tile - fixing a bug where it was possible for `userSwipedUpToApp` to be triggered when navigating back to the Story View from the deeplink destination ## **3.7.3** - 10/07/20 Improvements: - improving the performance of `getAdsForRow` so that it doesn't wait for the ads to be ready before opening the Story View ## **3.7.2** - 08/07/20 Bug Fixes: - fixing a crash when opening the Story View on Android 8 - fixing a bug where it was challenging to swipe up on stories - fixing a bug where the dismiss animation from the stories row did not happen in some situations - fixing a bug where it was possible for tracking pixels to be called multiple times in certain scenarios ## **3.7.1** - 07/07/20 Bug Fixes: - fixing a bug where if the `StorytellerRowView` was within a `RecyclerView` it was possible for there to be unexpected behavior when the view was recycled Improvements: - improving the performance of `setUserDetails` ## **3.7.0** - 06/07/20 Bug Fixes: - fixing a bug where the `adId` parameter was not present in certain scenarios for ad analytics events Improvements: - ensuring that `FinishedAd` also fires at the same time as `PreviousAd` - UI improvements and bug fixes New Features: - adding the `advertiserName` parameter to ad analytics events Deprecations and Changes: - rewriting the transition from the Story Row to the Story View ## **3.6.0** - 01/07/20 Bug Fixes: - fixing a bug where it was possible for Story events to fire for Ads - fixing a bug where it was possible for the SDK to crash when rendering certain client supplied ads - fixing a bug where it was possible for the SDK to crash when adding or removing pages to already published stories Improvements: - improving fault tolerance around network timeouts - identifying and fixing 2 memory leaks within the SDK Deprecations and Changes: - exposing a new `FinishedAd` event which rolls up the `DismissedAd`, `SkippedAd` and `ViewedAdPageComplete` events so it's easier to tell when an ad is not being presented to the user ## **3.5.2** - 20/06/20 - adding the ability to supply custom tracking pixels as part of the `getAdsForRow` request - improving the interaction between the SDK and the status bar - fixing a UI bug where the gradients in the loading tiles would not render correctly - fixing a crash on Android 8 ## **3.5.0** - 24/06/20 Bug Fixes: - fixing a bug where it was possible for the close button and native back button to work if the Story View was triggered from landscape mode - fixing a crash which could occur when swiping to particular stories - fixing a bug where the Story View did not exit when a user finished watching all of their unread stories - fixing a bug where single page stories were not being marked as read correctly - fixing a crash which could occur when closing the Story View Improvements: - improved the loading performance of ads UI Changes: - UI improvements and bugfixes ## **3.4.0** - 17/06/20 Bug Fixes: - fixing a crash which occurred when opening the Story View on Android 8 New Features: - UI improvements and bugfixes UI Changes: - adding support for swiping up to the App Store or to a location within the integrating app from a page or an ad - see [StorytellerRowViewDelegate](StorytellerDelegates.md) for more details ## **3.3.0** - 11/06/20 Bug Fixes: - fixing some bugs in the page navigation behavior around which pages are opened when stories are read/unread - fixing a bug where it was possible for `durationViewed` to be `0.0` in situations where the user had viewed content Improvements: - improving the transitions when tapping a tile in the story row to open the Story View and when pulling down to dismiss the Story View - improvements to preloading of stories to make swiping and tapping through the Story View feel much smoother - encrypting user access tokens for the Storyteller API at rest - ensuring `openedReason` is included on `OpenedAd` events - ensuring `openedReason` is included on `OpenedPage` events - ensuring that `DismissedAd` events report the `durationViewed` and `pagesViewedCount` properties New Features: - adding a `ShareSuccess` event to the SDK - adding the ability to supply ads to the SDK via the `getAdsForRow` callback on the [StorytellerRowViewDelegate](StorytellerDelegates.md) - adding new events for tracking user progress through ads - see [Analytics](Analytics.md) for specifics Deprecations and Changes: - when calling `openPage` or `openStory`, the `onFailure` callback will be called if the ID passed to `openPage` or `openStory` doesn't match a valid story or page - removing the `onSuccess` callback from `openPage` and `openStory`. If you would like to handle this case then please implement the `onUserActivityOccurred` callback and listen for `openedStory` or `openedPage` events with an `openedReason` of `deeplink` ## **3.2.0** - 03/06/20 Bug Fixes: - fixing a bug where the `openedReason` analytics property could have an incorrect value when the user swiped between stories Improvements: - improving the rendering performance of the StoryRowView when using the square layout ## **3.1.0** - 28/05/20 Bug Fixes: - fixing issue where `UserActivityData` class did not have publicly visible members New Features: - adding support for first-party ads loaded from the Storyteller CMS - emitting events related to ads playback via the `onUserActivityOccurred` callback - adding the ability to disable the story icon in the story view on a per-tenant basis - adding the ability to include a timestamp in the story view on a per-tenant basis - adding the ability to share a link configured in the CMS for each page (rather than the page's media) and the ability to configure this on a per-tenant basis ## **3.0.0** - 20/05/20 New Features: - (beta) adding `onUserActivityOccurred` callback to `StorytellerRowViewDelegate` so the containing app can receive notifications when users take actions within the SDK Deprecations and Changes: - removing `onChannelsDataLoadStarted`, `onChannelsDataLoadComplete` and `onChannelDismissed` callbacks from `StorytellerRowViewDelegate` - adding `onStoriesDataLoadStarted`, `onStoriesDataLoadComplete`, and `onStoryDismissed` callbacks to `StorytellerRowViewDelegate` - renaming public method `openChannel` to `openStory` - moving the store of read/unread pages from the server to the client --- ## Source: javadoc.md # Storyteller API Reference (Dokka) Please find the links to the generated API reference below: - [Main SDK (Dokka)](dokka/sdk/html/index.html) - [Ads Module (Dokka)](dokka/ads/html/index.html) --- ## Source: AI.md # Using Storyteller Kotlin SDK Docs with AI Assistants AI coding assistants (ChatGPT, Claude, Copilot, etc.) work best when you provide both: 1. Your app code (Gradle config, initialization code, and the UI integration point). 2. Storyteller Kotlin SDK documentation context. This page describes the recommended workflow. ## Download `llms.txt` (AI Context) - [llms.txt](ai/llms.txt) `llms.txt` aggregates the public docs into a single plain-text file that is easy to paste or attach to AI tools. Images are removed; headings and code examples are preserved. ## Recommended Workflow 1. Share `llms.txt` with the AI assistant (attach or paste). 2. Share the relevant app files/snippets: - Gradle setup (`settings.gradle(.kts)`, app `build.gradle(.kts)`) - SDK initialization (`Storyteller.initialize(...)`) - Share the integration entry point (Compose screen, Activity, Fragment) so the assistant understands the host UI. 3. Ask for a concrete output: a patch-style diff, a minimal snippet, or a step-by-step checklist. ## Rules for AI-Generated Code (Integration Safety) 1. Use only Storyteller public APIs that exist in the SDK and/or are documented in these pages. 2. Do not invent optional parameters, config flags, or DTO fields unless the prompt requires them. 3. Do not include secrets or PII in examples; use placeholders (e.g., `[APIKEY]`, `"user-id"`). 4. Prefer Jetpack Compose components unless legacy XML Views are explicitly required. 5. When referencing behavior or configuration, prefer the canonical docs URLs: - `https://docs.getstoryteller.com/android//` ## Good Prompt Examples - "Here is my `Application` and app `build.gradle.kts`. Add Storyteller initialization and a `StorytellerStoriesRow` to my Compose screen." - "I need to handle Storyteller deep links in `MainActivity`. Here is my intent handling code; update it safely." - "We want to disable personalization for some users. Show how to configure tracking options during initialization." ## Related Pages - [Quickstart Guide](GettingStarted.md) - [Working with Users](Users.md) - [Privacy and Tracking](PrivacyAndTracking.md) - [Storyteller Delegates](StorytellerDelegates.md)