diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt index 6553da67bc4..745dbaca18a 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt @@ -132,6 +132,7 @@ object Dependencies { const val composeActivity = "androidx.activity:activity-compose:${Versions.ANDROIDX_ACTIVITY_COMPOSE}" const val composeViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.ANDROIDX_LIFECYCLE}" const val composeStableMarker = "com.github.skydoves:compose-stable-marker:${Versions.COMPOSE_STABLE_MARKER}" + const val composeLifecycle = "androidx.lifecycle:lifecycle-runtime-compose:${Versions.ANDROIDX_LIFECYCLE}" const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.COROUTINES}" const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.COROUTINES}" const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.COROUTINES}" @@ -191,6 +192,7 @@ object Dependencies { const val navigationRuntimeKTX = "androidx.navigation:navigation-runtime-ktx:${Versions.ANDROIDX_NAVIGATION}" const val navigationTest = "androidx.navigation:navigation-testing:${Versions.ANDROIDX_NAVIGATION}" const val navigationUIKTX = "androidx.navigation:navigation-ui-ktx:${Versions.ANDROIDX_NAVIGATION}" + const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.ANDROIDX_NAVIGATION}" const val ok2curl = "com.github.mrmike:ok2curl:${Versions.OK2CURL}" const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.OKHTTP}" const val okhttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.OKHTTP}" diff --git a/docusaurus/android-docusaurus-dontent-docs.plugin.js b/docusaurus/android-docusaurus-dontent-docs.plugin.js index a88f71629d0..fc42c483779 100644 --- a/docusaurus/android-docusaurus-dontent-docs.plugin.js +++ b/docusaurus/android-docusaurus-dontent-docs.plugin.js @@ -13,6 +13,11 @@ module.exports = { path: 'v5', banner: 'unmaintained' }, + 'draft': { + label: 'Draft', + path: 'draft', + banner: 'unreleased' + }, } } ] diff --git a/docusaurus/android_versioned_docs/version-draft/01-basics/01-overview.mdx b/docusaurus/android_versioned_docs/version-draft/01-basics/01-overview.mdx new file mode 100644 index 00000000000..9c0f432dce4 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/01-basics/01-overview.mdx @@ -0,0 +1,122 @@ +--- +slug: / +--- + +# Overview + +The [Stream Chat Android SDK](https://github.com/GetStream/stream-chat-android) enables you to easily build any type of chat or messaging experience for Android, either in Kotlin or Java. + +:::note +The fastest way to get started with the SDK is by trying the [Android Views In-App Messaging Tutorial](https://getstream.io/tutorials/android-chat/). If you're using Jetpack Compose, see the [Compose In-App Messaging Tutorial](https://getstream.io/chat/compose/tutorial/) instead. +::: + +## Architecture + +The SDK consists of five major components: +- [Views/XML UI components](../02-ui-components/01-getting-started.mdx). Provides a set of reusable and customizable UI components based on Android Views. +- [Compose UI components](../03-compose/01-overview.mdx). Provides a set of reusable and customizable Jetpack Compose UI components. +- [Low-level client](https://getstream.io/chat/docs/android/?language=kotlin). Provides the main chat functionality. You can use it directly if you intend to build your own UI layer for the chat. +- [State](../05-state-and-offline/01-state-overview.mdx). Plugin that gives you the possibility to observe data via `StateFlows`. It exposes `StateFlow` objects containing the loading state, channel and message lists, channel members, users that are typing and more. +- [Offline Support](../05-state-and-offline/02-offline-support.mdx). Plugin that enables offline data and actions, like cached channels, messages, sending messages and reactions. Good for improving UX when the network connection is poor. + +We recommend using either the [Views/XML UI Components](../02-ui-components/01-getting-started.mdx) or the [Compose UI Components](../03-compose/01-overview.mdx) to most of our customers. If your UI doesn't significantly differ from the industry standard, you should be able to customize the built-in components to match your requirements. + +For more information about including each component in your project, see the [Installation](02-installation.mdx) page. + +## UI SDKs + +Built on top of the Stream Chat low-level client, the Stream Chat Android UI components enable you to easily build any type of chat or messaging experience for Android. + +We have UI component libraries available for both **Android Views** and **Jetpack Compose**. Each of these libraries provides an extensive collection of efficient and customizable UI components which enable you to quickly get started with little to no setup required. The libraries support: + +- Rich-media messages +- Channel and message lists +- Message Reactions +- Message threads and quoted replies +- Text input commands (ex: Giphy and @mentions) +- Image and file uploads +- Video playback +- Indicators for read state and typing +- Push notifications +- Offline storage +- Voice messages (only in the Android Views library for now) +- and more. + +If needed, you can create your own UI components by listening to the state we expose and using our components as building blocks. You can learn more about how state is exposed [here](../08-client/06-guides/04-state-plugin.mdx) and in the [low-level client docs](https://getstream.io/chat/docs/android/?language=kotlin). + +### Choosing the Right UI SDK + +#### Views UI Components + +The UI Components library includes pre-built Android Views to easily load and display data from the Stream Chat API. These include a Channel List and a Message List, a Message Composer View, and more. See the [Getting Started](../02-ui-components/01-getting-started.mdx) page for more details. + +This library is built on top of the state library, and offers the quickest integration of Stream Chat into an Android application. It also offers a variety of [theming](../02-ui-components/02-theming.mdx) options to make it fit your app's needs. + +You can see the UI Components in action by checking out the [UI Components Sample App](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components-sample), available in the GitHub repository. + +#### Compose UI Components + +The Compose UI Components library is a chat UI implementation built from scratch with [Jetpack Compose](https://developer.android.com/jetpack/compose). It contains modular Composable functions for building channel lists, messaging screens, and more. See the [Getting Started](../03-compose/01-overview.mdx) page for more details. + +This is also built on top of the state library, and offers easy integration of Stream Chat into a Compose-based Android application. It's also highly modular and customizable. + +Check out the Compose implementation in action by trying the open-source [Compose UI Components Sample App](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-compose-sample). + +:::note +Using our Compose SDK in an Android Views based app is fully supported. It could provide you an amazing opportunity to explore and adopt Compose. +::: + +#### The Right SDK for You + +If your use case requires a very high level of customization, runtime theming changes, replacing whole parts of the default UI or stateless components that don't rely on our ViewModels, +the **Compose SDK is the way to go**. It offers deep customization through several layers - the theme, modifiers for components, bound and stateless component overloads, smaller reusable components and slot APIs. + +If, instead, you're looking for a simpler, less customizable SDK which currently offers slightly better performance in some scenarios or your project is limited by technology, then +the **UI Components SDK is a great option**. It still offers a fair amount of customization through the theme and its attributes and some programmatic ways to customize the UI. + +Whichever SDK you choose, **we guarantee you'll be happy and we'll provide amazing support** with your integration, through detailed documentation, guides and direct support. + +## Upgrade and Versioning Strategy + +Our Android libraries do **not** follow semantic versioning. + +We increase the minor version whenever breaking changes are introduced. Patch releases only contain smaller fixes and improvements. + +You can see all the SDK changes in the [releases page](https://github.com/GetStream/stream-chat-android/releases), and you can also find the release notes of all past releases in our [CHANGELOG file](https://github.com/GetStream/stream-chat-android/blob/main/CHANGELOG.md). These will always highlight breaking changes. + +We also maintain a separate document, [DEPRECATIONS](https://github.com/GetStream/stream-chat-android/blob/main/DEPRECATIONS.md), which lists deprecated constructs in the SDK, with their expected time of further deprecations and eventual removal. + +:::info +You can get notified of new releases by using the _Watch_ button in the [getstream/stream-chat-android](https://github.com/GetStream/stream-chat-android) repository. You can tweak your watch preferences to subscribe only to release events. +::: + +### How Should I Specify My Dependency Version? + +You should use a fixed version in order to avoid any conflicts or unpredicted behaviour. + +For example: + +```groovy +implementation "io.getstream:stream-chat-android-compose:6.0.0" +``` + +### Snapshot Builds + +We publish SNAPSHOT versions of our SDK, which contain the code as of the latest commit on [the `develop` branch](https://github.com/GetStream/stream-chat-android/tree/develop). + +These builds may contain bugs or breaking changes that will be fixed before the next proper release. However, you can use these builds temporarily to include the latest changes from our SDK in your project before the next release happens. + +:::warning +Snapshot builds are not stable. Never use them in production. +::: + +To use snapshot builds, you need to add the Sonatype snapshot repository in your Gradle build configuration (see at the top of this page for where to add this): + +```groovy +maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } +``` + +Then you can add a snapshot dependency on any of our artifacts, replacing the normal version number with a version that has a `-SNAPSHOT` postfix. +Our snapshot version is always one patch version ahead of the latest release we've published. If the last stable release was `X.Y.Z`, the snapshot version would be `X.Y.(Z+1)-SNAPSHOT`. + +You can browse our available snapshot builds in the [Sonatype snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/io/getstream/), which you can also check for what the latest available snapshot version is. diff --git a/docusaurus/android_versioned_docs/version-draft/01-basics/02-installation.mdx b/docusaurus/android_versioned_docs/version-draft/01-basics/02-installation.mdx new file mode 100644 index 00000000000..311d68d99df --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/01-basics/02-installation.mdx @@ -0,0 +1,106 @@ +# Installation + +All Stream Android libraries are available from MavenCentral, with some of their transitive dependencies hosted on Jitpack. + +Before you add Stream dependencies, update your repositories in the `settings.gradle` file to include these two repositories: + +```groovy {5-6} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url "https://jitpack.io" } + } +} +``` + +Or if you're using an older project setup, add these repositories in your project level `build.gradle` file: + +```groovy {4-5} +allprojects { + repositories { + google() + mavenCentral() + maven { url "https://jitpack.io" } + } +} +``` + +Check the [Releases page](https://github.com/GetStream/stream-chat-android/releases) for the latest version and the changelog. + +[![Latest version badge](https://img.shields.io/github/v/release/GetStream/stream-chat-android)](https://github.com/GetStream/stream-chat-android/releases) + +## Available Artifacts + +### Client + +To add the low-level Chat client library to your app, open your module's `build.gradle` script and add the following: + +```groovy +dependencies { + implementation "io.getstream:stream-chat-android-client:$stream_version" +} +``` + +### State + +To use the state library in your application, add the following dependency: + +```groovy +dependencies { + implementation "io.getstream:stream-chat-android-state:$stream_version" +} +``` + +### Offline Support + +To use offline support in your application, add the following dependency: + +```groovy +dependencies { + implementation "io.getstream:stream-chat-android-offline:$stream_version" +} +``` + +This also adds the client library automatically. + +### UI Components + +To use the [UI Components](../02-ui-components/01-getting-started.mdx) in your application, add the following dependency: + +```groovy +dependencies { + implementation "io.getstream:stream-chat-android-ui-components:$stream_version" +} +``` + +Adding the UI Components library as a dependency will automatically include the client and offline libraries as well. + +### Compose UI Components + +To use the [Compose UI Components](../03-compose/01-overview.mdx) instead, add the following dependency: + +```groovy +dependencies { + implementation "io.getstream:stream-chat-android-compose:$stream_version" +} +``` + +Adding the Compose UI Components library as a dependency will automatically include the client and offline libraries as well. + +### Markdown support + +We ship an additional artifact for Markdown support which can be used together with the UI Components library: + + ```groovy + dependencies { + implementation "stream-chat-android-markdown-transformer:$stream_version" + } + ``` + + For more information see [UI Components Configuration](../02-ui-components/03-customizing-components.mdx#markdown) guide. + +### Push Notifications + +We ship multiple artifacts to easily integrate Stream Chat with third party push notification providers. See the [Push Notification page](../08-client/06-guides/01-push-notifications/01-overview.mdx) for more details. diff --git a/docusaurus/android_versioned_docs/version-draft/01-basics/03-getting-started.mdx b/docusaurus/android_versioned_docs/version-draft/01-basics/03-getting-started.mdx new file mode 100644 index 00000000000..8d69c3d2169 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/01-basics/03-getting-started.mdx @@ -0,0 +1,753 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Getting Started + +Let's see how you can get started with the Android Chat SDK after adding the required [dependencies](./02-installation.mdx). This page shows you how to initialize the SDK in your app. + +:::tip Step-by-step Tutorials +If you're looking for a complete, step-by-step guide that includes setting up an Android project from scratch, try our [Android In-App Messaging Tutorial](https://getstream.io/tutorials/android-chat/) or the [Jetpack Compose Android In-App Messaging Tutorial](https://getstream.io/chat/compose/tutorial/) in case you want to use our Compose powered Chat SDK. +::: + +## Setting up the ChatClient + +Your first step is initializing the `ChatClient`, which is the main entry point for all operations in the library. `ChatClient` is a singleton: you'll create it once and re-use it across your application. + +The best practice is to initialize `ChatClient` in the `Application` class: + + + + +```kotlin +class App : Application() { + override fun onCreate() { + super.onCreate() + val chatClient = ChatClient.Builder("apiKey", applicationContext).build() + } +} +``` + + + + +```java +class App extends Application { + @Override + public void onCreate() { + super.onCreate(); + ChatClient chatClient = new ChatClient.Builder("apiKey", getApplicationContext()).build(); + } +} +``` + + + +:::info Generating an API Key +To generate an API key, you can sign up for a [free 30-day trial](https://getstream.io/chat/trial/). You can then access your API key in the [Dashboard](https://getstream.io/dashboard). +::: + +If you create the `ChatClient` instance following the pattern shown in the previous example, you will be able to access that instance from any part of your application using the `instance()` method: + + + + +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val chatClient = ChatClient.instance() // Returns the singleton instance + } +} +``` + + + + +```java +class MainActivity extends AppCompatActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ChatClient chatClient = ChatClient.instance(); // Returns the singleton instance + } +} +``` + + + +:::info Logging +The _Builder_ for `ChatClient` exposes configuration options for features such as [Logging](06-advanced/01-logging.mdx). +::: + +## Handling User Connection + +This section gives you more insights on how to properly connect, disconnect or switch the user. + +### Connecting a User + +The next step is to connect the user. This requires a valid Stream Chat token. As you must use your `API_SECRET` to create this token, it is unsafe to generate this token outside of a secure server. + + + + +```kotlin +val user = User( + id = "bender", + name = "Bender", + image = "https://bit.ly/321RmWb", +) + +// Connect the user only if they aren't already connected +if (ChatClient.instance().getCurrentUser() == null) { + ChatClient.instance().connectUser(user = user, token = "userToken") // Replace with a real token + .enqueue { result -> + when (result) { + is Result.Success -> { + // Handle success + } + is Result.Failure -> { + // Handler error + } + } + } +} +``` + + + + +```java +User user = new User.Builder() + .withId("bender") + .withName("Bender") + .withImage("https://bit.ly/321RmWb") + .build(); + +// Connect the user only if they aren't already connected +if (ChatClient.instance().getCurrentUser() == null) { + ChatClient.instance().connectUser(user, "userToken") // Replace with a real token + .enqueue((result) -> { + if (result.isSuccess()) { + // Handle success + } else { + // Handle error + } + }); +} + +``` + + + +:::note +To learn more about how to create a token and different user types, see [Tokens & Authentication](https://getstream.io/chat/docs/android/tokens_and_authentication/?language=kotlin). +::: + +If the `connectUser()` call was successful, you are now ready to use the SDK. 🎉 + +:::warning +You shouldn't call `connectUser` if the user is already set. You can use `ChatClient.instance().getCurrentUser()` to verify if the user is already connected. +::: + +The methods of the `ChatClient` class allow you to create channels, send messages, add reactions, and perform many more low-level operations. You can also use the SDK's pre-built UI Components that will perform data fetching and sending for you, as described below. + +:::note +For more information on handling complex scenarios, check out our [Handling User Connection](../08-client/06-guides/02-handling-user-connection.mdx) guide. +::: + +### Disconnecting the User + +The user connection is automatically kept as long as the application is not killed. +However, you might want to explicitly disconnect the user, for example as a part of the logout flow. + + + + +```kotlin +ChatClient.instance().disconnect(flushPersistence = false).enqueue { result -> + when (result) { + is Result.Success -> { + // Handle success + } + is Result.Failure -> { + // Handle error + } + } +} +``` + + + + +```java +boolean flushPersistence = false; +ChatClient.instance().disconnect(flushPersistence).enqueue((result) -> { + if (result.isSuccess()) { + // Handle success + } else { + // Handle error + } +}); +``` + + + +Note that the `disconnect` method has an additional parameter that allows you to clear the database when using offline storage. +For more information about working with offline mode see [Offline Support](../../client/guides/offline-support) + +### Switching the User + +You might also want to switch the current user. In that case, the flow consists of disconnecting the currently logged-in user and connecting the new one. +Disconnecting is an asynchronous operation so you need to make sure to wait for its result before connecting the new user. +You can also use the `switchUser` method that disconnects the current user and connects the new one under the hood. + + + + +```kotlin +val user1 = User( + id = "bender", + name = "Bender", + image = "https://bit.ly/321RmWb", +) + +// Connect the first user +ChatClient.instance().connectUser(user = user1, token = "userToken") // Replace with a real token + .enqueue { result -> + when (result) { + is Result.Success -> { + // Handle success + } + is Result.Failure -> { + // Handle error + } + } + } + +val user2 = User( + id = "bender2", + name = "Bender2", + image = "https://bit.ly/321RmWb", +) + +ChatClient.instance().switchUser(user = user2, token = "userToken") // Replace with a real token + .enqueue { result -> + when (result) { + is Result.Success -> { + // Handle success + } + is Result.Failure -> { + // Handle error + } + } + } +``` + + + + +```java +User user1 = new User.Builder() + .withId("bender") + .withName("Bender") + .withImage("https://bit.ly/321RmWb") + .build(); + +// Connect the first user +ChatClient.instance().connectUser(user1, "userToken") // Replace with a real token + .enqueue((result) -> { + if (result.isSuccess()) { + // Handle success + } else { + // Handle error + } + }); + +User user2 = new User.Builder() + .withId("bender2") + .withName("Bender2") + .withImage("https://bit.ly/321RmWb") + .build(); + +ChatClient.instance().switchUser(user2, "userToken") // Replace with a real token + .enqueue((result) -> { + if (result.isSuccess()) { + // Handle success + } else { + // Handle error + } + }); +``` + + + +The snippet above will firstly connect `Bender` and after establishing the connection, disconnects and connects `Bender2`. + +## Channels + +Channels are where conversations take place between two or more chat users. They contain a list of messages and have a list of the member users that are participating in the conversation. A channel is identified by its `type` and `id`. Some APIs use a `cid` to identify a channel with a single string - this is the combination of the two pieces of information, as `type:id`. + +### Show Channel List + +You can query channels based on built-in fields as well as any custom field you add to channels. Multiple filters can be combined using AND, OR logical operators, each filter can use its comparison (equality, inequality, greater than, greater or equal, etc.). You can find the complete list of supported operators in the [query syntax section](https://getstream.io/chat/docs/android/query_syntax_operators/?language=kotlin) of the docs. + +As an example, let's say that you want to query the last conversations I participated in sorted by `last_message_at`. + +Here’s an example of how you can query the list of channels: + + + + +```kotlin +val request = QueryChannelsRequest( + filter = Filters.and( + Filters.eq("type", "messaging"), + Filters.`in`("members", listOf("thierry")), + ), + offset = 0, + limit = 10, + querySort = QuerySortByField.descByName("last_message_at") +).apply { + watch = true + state = true +} + +client.queryChannels(request).enqueue { result -> + when (result) { + is Result.Success -> { + val channels: List = result.value + } + is Result.Failure -> { + // Handle result.error() + } + } +} +``` + + + +```java +FilterObject filter = Filters.and( + Filters.eq("type", "messaging"), + Filters.in("members", Collections.singletonList("thierry")) +); +int offset = 0; +int limit = 10; +QuerySortByField sort = QuerySortByField.descByName("lastMessageAt"); +int messageLimit = 0; +int memberLimit = 0; + +QueryChannelsRequest request = new QueryChannelsRequest(filter, offset, limit, sort, messageLimit, memberLimit) + .withWatch() + .withState(); + +client.queryChannels(request).enqueue(result -> { + if (result.isSuccess()) { + List channels = result.getOrNull(); + } else { + // Handle error + } +}); +``` + + + +:::note +At a minimum, the filter should include members: { $in: [userID] }. +::: + +On messaging and team applications you normally have users added to channels as a member. A good starting point is to use this filter to show the channels the user is participating. + + + + +```kotlin +val filter = Filters.`in`("members", listOf("thierry")) +``` + + + +```java +FilterObject filter = Filters.in("members", Collections.singletonList("thierry")); +``` + + + +On a support chat, you probably want to attach additional information to channels such as the support agent handling the case and other information regarding the status of the support case (for example: open, pending, solved). + + + + +```kotlin +val filter = Filters.and( + Filters.eq("agent_id", user.id), + Filters.`in`("status", listOf("pending", "open", "new")), +) +``` + + + +```java +FilterObject filter = Filters.and( + Filters.eq("agent_id", user.getId()), + Filters.in("status", Arrays.asList("pending", "open", "new")) +); +``` + + + +### Creating Channels + +If you need to create a channel, you can use `channel.create` and pass a `channelId`. + + + + +```kotlin +val channelClient = client.channel(channelType = "messaging", channelId = "general") + +channelClient.create(memberIds = emptyList(), extraData = emptyMap()).enqueue { result -> + when (result) { + is Result.Success -> { + val newChannel: Channel = result.value + } + is Result.Failure -> { + // Handler error + } + } +} +``` + + + +```java +ChannelClient channelClient = client.channel("messaging", "general"); + +Map extraData = new HashMap<>(); +List memberIds = new LinkedList<>(); + +channelClient.create(memberIds, extraData) + .enqueue(result -> { + if (result.isSuccess()) { + Channel newChannel = result.getOrNull(); + } else { + // Handle error + } + }); +``` + + + +## Client Plugins + +Plugins offer a convenient way of adding additional functionality to `ChatClient`, without the need for introducing additional classes or complex wrappers. Any [`Plugin`](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt) that can be produced by a [`PluginFactory`](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/factory/PluginFactory.kt) can be added to `ChatClient`. + +Adding a plugin is easy: + + + + +```kotlin {2-4} +val client = ChatClient.Builder(apiKey, context) + .withPlugins( + //Add the desired plugin factories here + ) + .build() +``` + + + + +```java {2-4} +new ChatClient.Builder(apiKey, context) + .withPlugins( + //Add the desired plugin factories here + ) + .build(); +``` + + + +### Adding State + +Our UI Components rely on reading the state in order to render the UI. Information such as the currently active user, list of muted users, online status, etc. is saved as state inside various state holders (for example`GlobalState`, and others). The state is managed by `StatePlugin` and adding it to `ChatClient` **is mandatory if you want to use our UI Components**. + +Let's see how we can add the plugin: + + + + +```kotlin +// Create a state plugin factory +val statePluginFactory = StreamStatePluginFactory( + config = StatePluginConfig( + // Enables background sync which syncs user actions performed while offline + backgroundSyncEnabled = true, + // Enables tracking online states for users + userPresence = true + ), + appContext = context +) + +ChatClient.Builder(apiKey, context) + // Add the state plugin to the chat client + .withPlugins(statePluginFactory) + .build() +``` + + + + + +```java +// Enable background sync which syncs user actions performed while offline +boolean backgroundSyncEnabled = true; +// Enable tracking online states for users +boolean userPresence = true; + +// Create a state plugin factory +StreamStatePluginFactory statePluginFactory = new StreamStatePluginFactory( + new StatePluginConfig( + backgroundSyncEnabled, + userPresence + ), + context.getApplicationContext() +); + +new ChatClient.Builder(apiKey, context) + // Add the state plugin to the chat client + .withPlugins(statePluginFactory) + .build(); +``` + + + + +### Adding Offline Support + +A great chat solution should be capable of caching data and handling network connection loss.
+`OfflinePlugin` implements such capabilities and provides the following benefits: + +* Decreases initial load times, especially when the network connection is poor. +* Enables the user to see cached channels and messages while offline. +* Enables the user to perform actions such as sending messages and reactions while offline. + +:::note +`OfflinePlugin` works best when used together with `StatePlugin`. +::: + +First, add the dependency which contains offline support: + +```groovy {2} +dependencies { + implementation "io.getstream:stream-chat-android-offline:$stream_version" +} +``` + +Then, you can add the plugin in the following manner: + + + + +```kotlin +// Create an offline plugin factory +val offlinePluginFactory = StreamOfflinePluginFactory(appContext = context) + +// Create a state plugin factory +val statePluginFactory = StreamStatePluginFactory( + config = StatePluginConfig( + // Enables background sync which syncs user actions performed while offline. + backgroundSyncEnabled = true, + // Enables tracking online states for users + userPresence = true, + ), + appContext = context +) + +ChatClient.Builder(apiKey, context) + // Add both the state and offline plugin factories to the chat client + .withPlugins(offlinePluginFactory, statePluginFactory) + .build() +``` + + + + +```java +// Create an offline plugin factory +StreamOfflinePluginFactory offlinePluginFactory = new StreamOfflinePluginFactory(context); + +// Enable background sync which syncs user actions performed while offline +boolean backgroundSyncEnabled = true; +// Enable tracking online states for users +boolean userPresence = true; + +// Create a state plugin factory +StreamStatePluginFactory statePluginFactory = new StreamStatePluginFactory( + new StatePluginConfig( + backgroundSyncEnabled, + userPresence + ), + context.getApplicationContext() +); + +new ChatClient.Builder(apiKey, context) + // Add both the state and offline plugin factories to the chat client + .withPlugins(offlinePluginFactory, statePluginFactory) + .build(); +``` + + + +For more information on working with `OfflinePlugin`, see [Offline Support](../08-client/06-guides/03-offline-support.mdx) + +## Calls + +Many SDK methods in the client and offline libraries return a `Call` object, which is a pending operation waiting to be executed. + +### Running Calls Synchronously + +If you're on a background thread, you can run a `Call` synchronously, in a blocking way, using the `execute` method: + + + + +```kotlin +// Only call this from a background thread +val messageResult = channelClient.sendMessage(message).execute() +``` + + + + +```java +// Only call this from a background thread +Result messageResult = channelClient.sendMessage(message).execute(); +``` + + + +### Running Calls Asynchronously + +You can run a `Call` asynchronously, automatically scheduled on a background thread using the `enqueue` method. The callback passed to `enqueue` will be called on the UI thread. + + + + +```kotlin +// Safe to call from the main thread +channelClient.sendMessage(message).enqueue { result: Result -> + when (result) { + is Result.Success -> { + val sentMessage: Message = result.value + } + is Result.Failure -> { + // Handle error + } + } +} +``` + + + + +```java +// Safe to call from the main thread +channelClient.sendMessage(message).enqueue((result) -> { + if (result.isSuccess()) { + Message sentMessage = result.data(); + } else { + // Handle result.error() + } +}); +``` + + + + +If you are using Kotlin coroutines, you can also `await()` the result of a `Call` in a suspending way: + +```kotlin +viewModelScope.launch { + // Safe to call from any CoroutineContext + val messageResult = channelClient.sendMessage(message).await() +} +``` + +### Error Handling + +Actions defined in a `Call` return `Result` objects. These contain either the result of a successful operation or the error that caused the operation to fail. + +If the result is successful, you can get the contained data. Otherwise, you can fetch the error and handle it appropriately. + + + + +```kotlin +when (result) { + is Result.Success -> { + val channel: Channel = result.value + // Handle success + } + is Result.Failure -> { + val error: Error = result.value + // Handler error + } +} +``` + + + + +```java +// Check if the call was successful +Boolean isSuccess = result.isSuccess(); +// Check if the call had failed +Boolean isFailure = result.isFailure(); + +if (result.isSuccess()) { + // Handle success + Channel channel = result.getOrNull(); +} else { + // Handle error + Error error = result.errorOrNull(); +} +``` + + + +Alternatively, you can handle `Call` operations in a reactive way by using operators: + + + + +```kotlin +result.onSuccess { channel -> + // Handle success +}.onError { error -> + // Handle error +} +``` + + + + +```java +result.onSuccess(channel -> { + // Handle success + return null; +}).onError(error -> { + // Handle error + return null; +}); +``` + + + +## Adding UI Components + +There are two UI Component implementations available: one built on regular, XML based Android Views, and another built from the ground up in [Jetpack Compose](https://developer.android.com/jetpack/compose). + +Take a look at the Overview pages of the implementations to get started with them: +- [XML based UI Components](../02-ui-components/01-getting-started.mdx) +- [Jetpack Compose UI Components](../03-compose/01-overview.mdx) diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/01-getting-started.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/01-getting-started.mdx new file mode 100644 index 00000000000..a8183d660ac --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/01-getting-started.mdx @@ -0,0 +1,114 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Getting Started + +The **UI Components** library includes pre-built Android Views to easily load and display data from the Stream Chat API. + +:::info +Already using Jetpack Compose? Check out our [Compose UI Components](../03-compose/01-overview.mdx). +::: + +| ![Channel List component](../assets/sample-channels-light.png) | ![Message List component](../assets/sample-messages-light.png) | +| --- | --- | + +This library builds on top of the offline library, and provides [ViewModels](#viewmodels) for most Views to easily populate them with data and handle input events. + +The [sample app](#sample-app) showcases the UI components in action. + +See the individual pages of the components to learn more about them. + +**Channel components**: + +- [Channel List Screen](04-channel-list/01-channel-list-screen.mdx) +- [Channel List](04-channel-list/03-channel-list.mdx) +- [Channel List Header](04-channel-list/02-channel-list-header.mdx) + +**Message components**: + +- [Message List Screen](05-message-list/01-message-list-screen.mdx) +- [Message List](05-message-list/03-message-list.mdx) +- [Message List Header](05-message-list/02-message-list-header.mdx) +- [Message Composer](06-message-composer/01-message-composer.mdx) + +**Utility components**: + +- [Mention List](05-message-list/05-mentions-and-pinned-messages.mdx#mention-list) +- [Pinned Message List](05-message-list/05-mentions-and-pinned-messages.mdx#pinned-message-list) +- [Search Views](05-message-list/04-search-view.mdx) +- [Attachment Gallery](06-message-composer/02-working-with-attachments.mdx) + +## Requirements + +To use the UI Components, add the dependency to your app, as described on the [Dependencies](../01-basics/02-installation.mdx#ui-components) page. + +Since this library uses Material elements, make sure that you use a Material theme in your application before adding the components. This means that your app's theme should extend a theme from `Theme.MaterialComponents`, and not `Theme.AppCompat`. Here's a correct example: + +```xml + +``` +The theme you pass in as the `streamUiTheme` can then define a style for each type of UI Component where you can set attribute values. + +For example, you can achieve the same styling like in the examples above by overriding the `streamUiMessageListStyle` attribute: + +```xml + + + +``` + +:::note +The list of available styles you can define here is available here in our [`attrs` file](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs.xml). +::: + +## Themes for Activities + +SDK contains the following activities: `AttachmentMediaActivity`, `AttachmentActivity` and `AttachmentGalleryActivity`. You can customize them by overriding the activity with a custom `theme` in your manifest. + +Let's see how to change the color of the title on the gallery screen: + +**AndroidManifest.xml** + +```xml + +``` + +**themes.xml** + +```xml + + + + + + + +``` + +This will produce the UI below: + +| ![Custom activity theme](../assets/custom_activity_theme.png) | +|-------------------------------------------------------------------| + +## Choose Light/Dark Theme + +Our SDK already provides a DayNight theme. If you want to force Dark or Light mode, you need to follow the default Android mechanism to use it: + + + + +```kotlin +// Force Dark theme +AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + +// Force Light theme +AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) +``` + + + + +```java +// Force Dark theme +AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + +// Force Light theme +AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); +``` + + diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/03-customizing-components.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/03-customizing-components.mdx new file mode 100644 index 00000000000..cc537cbcd36 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/03-customizing-components.mdx @@ -0,0 +1,973 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Customizing Components + +The SDK provides an API for general configuration of the UI Component library's behavior and appearance, which is exposed via the `ChatUI` object. + +`ChatUI` allows you to override the default implementations of commonly used parts of the SDK such as: + +* Available message reactions +* The UI used for rendering attachments +* MIME type icons for attachments +* Default font used across the UI components +* Attachments URLs +* Text transformations + +The full list of `ChatUI` properties you can override include: + +* `style`: Allows overriding the global, default style of UI components, such as `defaultTextStyle`. +* `navigator`: Allows intercepting and modifying default navigation between SDK components (for example: navigating from `MessageListView` to `AttachmentGalleryActivity`). +* `imageHeadersProvider`: Allows adding extra headers to image loading requests. +* `fonts`: The default font for `TextView`s displayed by UI Components. +* `messageTextTransformer`: Used to transform the way text is rendered on screen, for example: create clickable link text or implement markdown support. You can override it with `MarkdownTextTransformer` if you want to use Stream's ready-made markdown support. +* `supportedReactions`: The set of supported message reactions. +* `mimeTypeIconProvider`: The icons used for different mime types. +* `channelNameFormatter`: Allows customizing the way channel names are formatted. +* `messagePreviewFormatter`: Allows you to generate a preview text for the given message. +* `dateFormatter`: Allows changing the way dates are formatted. +* `attachmentFactoryManager`: Allows changing the way attachments are displayed in the message list. Includes adding UI for custom attachments. +* `attachmentPreviewFactoryManager`: Allows changing the way attachments are displayed in the message composer. Includes adding UI for custom attachments. +* `quotedAttachmentFactoryManager`: Allows changing the way attachments are displayed in quoted messages both in the message list and the message composer. Includes adding UI for custom attachments. +* `currentUserProvider`: provides the currently logged in user. +* `videoThumbnailsEnabled`: Changes whether video thumbnails are displayed or not. Video thumbnails are a paid feature, You can find the pricing [here](https://getstream.io/chat/pricing/). + +:::note +`ChatUI` is initialized out-of-the-box with default implementations - no initialization is required on app startup. +::: + +## Custom Reactions + +By default, the SDK provides 5 built-in reactions: + +| Light Theme | Dark Theme | +|--------------------------------------------------------------------------|------------------------------------------------------------------------------| +| ![Default reactions light theme](../assets/chatui_default_reactions.png) | ![Default reactions dark theme](../assets/chatui_default_reactions_dark.png) | + +You can change the default reactions by overriding `ChatUI.supportedReactions` with your own set of reactions: + + + + + +```kotlin +// Create a drawable for the non-selected reaction option +val loveDrawable = ContextCompat.getDrawable(context, R.drawable.stream_ui_ic_reaction_love)!! +// Create a drawable for the selected reaction option and set a tint to it +val loveDrawableSelected = ContextCompat.getDrawable(context, R.drawable.stream_ui_ic_reaction_love)!! + .mutate() + .apply { setTint(Color.RED) } + +// Create a map of reactions +val supportedReactionsData = mapOf( + "love" to SupportedReactions.ReactionDrawable(loveDrawable, loveDrawableSelected) +) + +// Replace the default reactions with your custom reactions +ChatUI.supportedReactions = SupportedReactions(context, supportedReactionsData) +``` + + + + +```java +// Create a drawable for the non-selected reaction option +Drawable loveDrawable = ContextCompat.getDrawable(context, R.drawable.stream_ui_ic_reaction_love); +// Create a drawable for the selected reaction option and set a tint to it +Drawable loveDrawableSelected = ContextCompat.getDrawable(context, R.drawable.stream_ui_ic_reaction_love).mutate(); +loveDrawableSelected.setTint(Color.RED); + +// Create a map of reactions +Map supportedReactionsData = new HashMap<>(); +supportedReactionsData.put("love", new SupportedReactions.ReactionDrawable(loveDrawable, loveDrawableSelected)); + +// Replace the default reactions with your custom reactions +ChatUI.setSupportedReactions(new SupportedReactions(context, supportedReactionsData)); +``` + + + +As a result, only the _love_ reaction is available in the chat, and when selected, it will have a red tint. + +| Normal state - available reactions (Light Mode) | Selected state - reaction selected (Light Mode) | +| --- | --- | +|![Normal state Available Reactions Light Mode](../assets/chat_ui_custom_reaction.png)|![Selected state Available Reactions Light Mode](../assets/chat_ui_custom_reaction_active.png)| + +| Normal state - available reactions (Dark Mode) | Selected state - reaction selected (Dark Mode) | +| --- | --- | +|![Normal state Available Reactions Dark Mode](../assets/chat_ui_custom_reaction_dark.png)|![Normal state Available Reactions Dark Mode](../assets/chat_ui_custom_reaction_active_dark.png)| + +## Custom MIME Type Icons + +When possible, the SDK displays thumbnails for image and video files. When thumbnails are unavailable or when other types of files are in question, mime type icons are displayed in `MessageListView`, `MessageComposer` and attachment picker. + +By default, the SDK provides built-in MIME type icons for the most popular file types and displays a generic file icon for others. + +To customize these icons, you need to override `ChatUI.mimeTypeIconProvider` like so: + + + + +```kotlin +ChatUI.mimeTypeIconProvider = MimeTypeIconProvider { mimeType -> + when { + // Generic icon for missing MIME type + mimeType == null -> R.drawable.stream_ui_ic_file + // Special icon for XLS files + mimeType == "application/vnd.ms-excel" -> R.drawable.stream_ui_ic_file_xls + // Generic icon for audio files + mimeType.contains("audio") -> R.drawable.stream_ui_ic_file_mp3 + // Generic icon for video files + mimeType.contains("video") -> R.drawable.stream_ui_ic_file_mov + // Generic icon for other files + else -> R.drawable.stream_ui_ic_file + } +} +``` + + + + +```java +ChatUI.setMimeTypeIconProvider(mimeType -> { + if (mimeType == null) { + // Generic icon for missing MIME type + return R.drawable.stream_ui_ic_file; + } else if (mimeType.equals("application/vnd.ms-excel")) { + // Special icon for XLS files + return R.drawable.stream_ui_ic_file_xls; + } else if (mimeType.contains("audio")) { + // Generic icon for audio files + return R.drawable.stream_ui_ic_file_mp3; + } else if (mimeType.contains("video")) { + // Generic icon for video files + return R.drawable.stream_ui_ic_file_mov; + } else { + // Generic icon for other files + return R.drawable.stream_ui_ic_file; + } +}); +``` + + + +## Customizing Avatars +An avatar is a small image which identifies a specific user or channel. +`UserAvatarView` is used in the lists of users and messages, whereas `ChannelAvatarView` is used in the lists of channels. + +The image in the `UserAvatarView` and `ChannelAvatarView` is being displayed based on the `image` property, present in both `User` and `Channel` objects respectively: + +| Light Mode | Dark Mode | +|---|---| +| ![Default Avatar Light Mode](../assets/default_channel.png) | ![Default Avatar Dark Mode](../assets/default_channel_dark.png) | + +Both `UserAvatarView` and `ChannelAvatarView` will use the default gradient color with initials if the `image` property cannot be loaded: + +| Light Mode | Dark Mode | +|---|---| +| ![Default Avatar Light Mode](../assets/default_avatar.png) | ![Default Avatar Dark Mode](../assets/default_avatar_dark.png) | + +In addition, `ChannelAvatarView` provides several extra fallback scenarios if `image` property is an empty string: +- If the channel has just `1` member, the `image` property of this user will be used to display an avatar. +- If the channel has just `2` members, the `name` property of the user, who is not the current user will be used to display an avatar. +- If the channel has more than two members, the avatar image will be produced from the first `4` users in the channel's member list. + +#### Customizing Avatars Using Styles +You can configure the avatar shape, border width, online indicator and other aspects using [AvatarStyle](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/widgets/avatar/AvatarStyle.kt). You can create this kind of avatar by changing the shape and corner radius: + +| Light Mode | Dark Mode | +|---|---| +| ![Light Mode](../assets/square_avatar.png) | ![Dark Mode](../assets/square_avatar_dark.png) + +#### Customizing User Avatars Using UserAvatarRenderer + +Overriding the `UserAvatarRenderer` allows you to add custom logic for the user's avatars displayed using `UserAvatarView`. + +:::note +The `UserAvatarView.setAvatar` method mentioned below can accept different data types as `avatar` parameter. +It can be a `Bitmap`, `Drawable`, `Uri`, `String` or `DrawbleRes` (resource id). +::: + + + + +```kotlin +ChatUI.userAvatarRenderer = object : UserAvatarRenderer { + override fun render(style: AvatarStyle, user: User, target: UserAvatarView) { + // You can apply custom image loading logic here + val placeholder: Drawable = /* ... */ + target.setAvatar(avatar = user.image, placeholder = placeholder) + target.setOnline(online = user.online) + } +} +``` + + + + +```java +final UserAvatarRenderer renderer = new UserAvatarRenderer() { + @Override + public void render( + @NonNull AvatarStyle style, + @NonNull User user, + @NonNull UserAvatarView target + ) { + // You can apply custom image loading logic here + final Drawable placeholder = /* ... */; + target.setAvatar(user.getImage(), placeholder); + target.setOnline(user.getOnline()); + } +}; + +ChatUI.setUserAvatarRenderer(renderer); +``` + + + +#### Customizing Channel Avatars Using ChannelAvatarRenderer + +Overriding the `ChannelAvatarRenderer` allows you to add custom logic for the channel's avatars displayed using `ChannelAvatarView`. + +:::note +The `AvatarImageView.setAvatar` method mentioned below can accept different data types as `avatar` parameter. +It can be a `Bitmap`, `Drawable`, `Uri`, `String` or `DrawbleRes` (resource id). +::: + +Let's look at the different scenarios for channel avatars, which are handled by the default. +You can override the `ChannelAvatarRenderer` to apply custom logic for each scenario or just simplify the logic to a single approach for all scenarios. + + + + +```kotlin +ChatUI.channelAvatarRenderer = object : ChannelAvatarRenderer { + override fun render( + style: AvatarStyle, + channel: Channel, + user: User, + targetProvider: ChannelAvatarViewProvider, + ) { + // You can apply custom image loading logic here + val placeholder: Drawable = /* ... */ + + // Scenario_1: `Channel.image` is not empty + val target1: AvatarImageView = targetProvider.regular() + target1.setAvatar(avatar = channel.image, placeholder = placeholder) + + // Scenario_2: `Channel.image` is empty and `Channel` has less or equal to 2 members + val singleUser: User = /* ... */ + val target2: UserAvatarView = targetProvider.singleUser() + target2.setAvatar(avatar = singleUser.image, placeholder = placeholder) + target2.setOnline(online = singleUser.online) + + // Scenario_3: `Channel.image` is empty and `Channel` has more than 2 members + val users = channel.members.filter { it.user.id != currentUser?.id }.map { it.user } + val target3: List = targetProvider.userGroup(users.size) + target3.forEachIndexed { index, targetItem -> + targetItem.setAvatar(avatar = users[index].image, placeholder = placeholder) + } + } +} +``` + + + + +```java +ChannelAvatarRenderer renderer = new ChannelAvatarRenderer() { + @Override + public void render( + @NonNull AvatarStyle style, + @NonNull User user, + @NonNull UserAvatarView target + ) { + // You can apply custom image loading logic here + final Drawable placeholder = /* ... */; + + // Scenario_1: `Channel.image` is not empty + final AvatarImageView target1 = targetProvider.regular(); + target1.setAvatar(channel.getImage(), placeholder); + + // Scenario_2: `Channel.image` is empty and `Channel` has less or equal to 2 members + final User singleUser = /* ... */; + final UserAvatarView target2 = targetProvider.singleUser(); + target2.setAvatar(singleUser.getImage(), placeholder); + target2.setOnline(singleUser.getOnline()); + + // Scenario_3: `Channel.image` is empty and `Channel` has more than 2 members + final List users = channel.getMembers().stream() + .filter(member -> !member.getUser().getId().equals(currentUser.getId())) + .map(Member::getUser) + .collect(Collectors.toList()); + final List target3 = targetProvider.userGroup(users.size()); + for (int i = 0; i < target3.size(); i++) { + target3.get(i).setAvatar(users.get(i).getImage(), placeholder); + } + } +}; + +ChatUI.setChannelAvatarRenderer(renderer); +``` + + + + + +**If you only would like to change the gradient colors for the default avatar**, you can use `stream_ui_avatar_gradient_colors`. + +The default color set includes a variety of colors: + +| Light Mode | Dark Mode | +|---|---| +| ![Colorful Avatars Light Mode](../assets/colorful_avatars.png) | ![Colorful Avatars Dark Mode](../assets/colorful_avatars_dark.png) | + +The set can be overridden in the `color.xml` file - you can expand or reduce the number of supported colors, like in the example below: + +``` + + @color/stream_ui_avatar_gradient_blue + +``` + +Which creates: + +| Light Mode | Dark Mode | +|---|---| +| ![Blue Gradient Avatars Light Mode](../assets/blue_gradients_avatars.png) | ![Blue Gradient Avatars Dark Mode](../assets/blue_gradients_avatars_dark.png) | + +## Adding Extra Headers to Image Requests + +If you're [using your own CDN](https://getstream.io/chat/docs/android/file_uploads/?language=kotlin#using-your-own-cdn), you might also need to add extra headers to image loading requests. You can do this by creating your own implementation of the `ImageHeadersProvider` interface and then setting it on `ChatUI`: + + + + +```kotlin +ChatUI.imageHeadersProvider = object : ImageHeadersProvider { + override fun getImageRequestHeaders(): Map { + return mapOf("token" to "12345") + } +} +``` + + + + +```java +ChatUI.setImageHeadersProvider(() -> { + Map headers = new HashMap<>(); + headers.put("token", "12345"); + + return headers; +}); +``` + + + +## Changing the Default Font + +You can customize the default fonts used by all of the UI components. To change the fonts, implement the `ChatFont` interface and set the new implementation on `ChatUI`: + + + + +```kotlin +ChatUI.fonts = object : ChatFonts { + + // Fetch the font you want to use + val font = ResourcesCompat.getFont(context, R.font.stream_roboto_regular) + + override fun setFont(textStyle: TextStyle, textView: TextView) { + textView.setTypeface(font, Typeface.BOLD) + } + + override fun setFont(textStyle: TextStyle, textView: TextView, defaultTypeface: Typeface) { + textView.setTypeface(font, Typeface.BOLD) + } + + override fun getFont(textStyle: TextStyle): Typeface? = font +} +``` + + + + +```java +ChatUI.setFonts(new ChatFonts() { + + // Fetch the font you want to use + final Typeface font = ResourcesCompat.getFont(context, R.font.stream_roboto_regular); + + @Override + public void setFont(@NonNull TextStyle textStyle, @NonNull TextView textView) { + textView.setTypeface(font, Typeface.BOLD); + } + + @Override + public void setFont(@NonNull TextStyle textStyle, @NonNull TextView textView, @NonNull Typeface defaultTypeface) { + textView.setTypeface(font, Typeface.BOLD); + } + + @Nullable + @Override + public Typeface getFont(@NonNull TextStyle textStyle) { + return font; + } +}); +``` + + + +## Transforming Message Text + +You can easily provide a transformer that can transform and apply the message text to a given `TextView`. You need to override `ChatUI.messageTextTransformer` to an instance of `ChatMessageTextTransformer`s implementation. + + + + +```kotlin +ChatUI.messageTextTransformer = ChatMessageTextTransformer { textView: TextView, messageItem: MessageItem -> + // Transform messages to upper case. + textView.text = messageItem.message.text.uppercase() +} +``` + + + + +```java +ChatUI.setMessageTextTransformer((textView, messageItem) -> { + textView.setText(messageItem.getMessage().getText().toUpperCase(Locale.ROOT)); +}); +``` + + + +Stream UI TextView components don't have `android:autoLink` property set because it conflicts with Markdown plugins. +:::note +You can use `AutoLinkableTextTransformer` if you want to apply custom transformation but keep links clickable. +::: + +## Markdown + +The SDK provides a standalone Markdown module `stream-chat-android-markdown-transformer` that contains `MarkdownTextTransformer` which is an implementation of `ChatMessageTextTransformer`. It uses the [Markwon](https://github.com/noties/Markwon) library internally. + + + + +```kotlin +ChatUI.messageTextTransformer = MarkdownTextTransformer(context) +``` + + + + +```java +ChatUI.setMessageTextTransformer(new MarkdownTextTransformer(context)); +``` + + + +If you use `MarkdownTextTransformer`, don't use `android:autoLink` attribute because it'll break the markdown [Linkify](https://noties.io/Markwon/docs/v4/linkify/) implementation. + +Then the SDK will parse Markdown automatically: + +| Markdown Input in the Message Composer | Message with Markdown in the Message List | +|---|---| +| ![Markdown Input in the Message Composer](../assets/markdown_support.png) | ![Markdown Message in the Message List](../assets/markdown_support_result.png) | + +## Navigator + +The SDK performs navigation in certain cases: + +- Navigating to `AttachmentGalleryActivity` after clicking on a video or an image attachment. +- Opening the browser after clicking a link in the chat. + +This action is performed by `ChatNavigator`. You can customize its behavior by providing your own implementation of `ChatNavigationHandler`: + + + + +```kotlin +val navigationHandler = ChatNavigationHandler { destination: ChatDestination -> + // Perform a custom action here + true +} + +ChatUI.navigator = ChatNavigator(navigationHandler) +``` + + + + +```java +ChatNavigationHandler chatNavigatorHandler = destination -> { + // Perform a custom action here + return true; +}; + +ChatUI.setNavigator(new ChatNavigator(chatNavigatorHandler)); +``` + + + +## Customizing ChannelNameFormatter + +You can customize the way channel names are formatted by overriding the default `ChannelNameFormatter`: + + + + +```kotlin +ChatUI.channelNameFormatter = ChannelNameFormatter { channel, currentUser -> + channel.name +} +``` + + + + +```java +ChatUI.setChannelNameFormatter((channel, currentUser) -> channel.getName()); +``` + + + +## Customizing MessagePreviewFormatter + +You can change the way the last messages are formatted in the channel list by overriding the default `MessagePreviewFormatter`: + + + + +```kotlin +ChatUI.messagePreviewFormatter = MessagePreviewFormatter { channel, message, currentUser -> + message.text +} +``` + + + + +```java +ChatUI.setMessagePreviewFormatter((channel, message, currentUser) -> message.getText()); +``` + + + +## Customizing DateFormatter + +Overriding the `DateFormatter` allows you to change the way dates are formatted in the application: + + + + +```kotlin +ChatUI.dateFormatter = object: DateFormatter { + private val dateFormat: DateFormat = SimpleDateFormat("dd/MM/yyyy") + private val timeFormat: DateFormat = SimpleDateFormat("HH:mm") + + override fun formatDate(date: Date?): String { + date ?: return "" + return dateFormat.format(date) + } + + override fun formatTime(date: Date?): String { + date ?: return "" + return timeFormat.format(date) + } +} +``` + + + + +```java +ChatUI.setDateFormatter(new DateFormatter() { + private final DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy"); + private final DateFormat timeFormat = new SimpleDateFormat("HH:mm"); + + public String formatDate(Date date) { + // Provide a way to format Date + return dateFormat.format(date); + } + + public String formatTime(Date date) { + // Provide a way to format Time + return timeFormat.format(date); + } +}); + +``` + + + +## Customizing Attachments + +Stream allows both overriding the way the out-of-the-box supported attachments (image, video, etc.) are displayed, and implementing UI for custom attachments. + +In the intro section, we've mentioned the different types of attachment factory managers. Depending on which part of the UI you want to customize, you will have to override different `ChatUI` properties. + +If you want to customize the way attachments are presented in `MessageListView`: + + + + +```kotlin +val attachmentFactoryManager = AttachmentFactoryManager( + // Set your custom attachment factories here +) + +ChatUI.attachmentFactoryManager = attachmentFactoryManager +``` + + + + +```java +AttachmentFactoryManager attachmentFactoryManager = new AttachmentFactoryManager( + // Set your custom attachment factories here +); + +ChatUI.setAttachmentFactoryManager(attachmentFactoryManager); +``` + + + +If you want to customize the way attachments are presented in `MessageInputView` or `MessageComposerView`: + + + + +```kotlin +val attachmentPreviewFactoryManager = AttachmentPreviewFactoryManager( + // Set your custom attachment factories here +) + +ChatUI.attachmentPreviewFactoryManager = attachmentPreviewFactoryManager +``` + + + + +```java +AttachmentPreviewFactoryManager attachmentPreviewFactoryManager = new AttachmentPreviewFactoryManager( + // Set your custom attachment factories here +); + +ChatUI.setAttachmentPreviewFactoryManager(attachmentPreviewFactoryManager); +``` + + + +If you want to customize the way attachments are presented inside quoted messages in `MessageListView`: + + + + +```kotlin +val quotedAttachmentFactoryManager = QuotedAttachmentFactoryManager( + // Set your custom attachment factories here +) + +ChatUI.quotedAttachmentFactoryManager = quotedAttachmentFactoryManager +``` + + + + +```java +QuotedAttachmentFactoryManager quotedAttachmentFactoryManager = new QuotedAttachmentFactoryManager( + // Set your custom attachment factories here +); + +ChatUI.setQuotedAttachmentFactoryManager(quotedAttachmentFactoryManager); +``` + + + +### Guide on Adding Support for Custom Attachments + +We offer a [guide](06-message-composer/04-custom-attachments.mdx) on implementing custom attachment support which covers the topic extensively. + +## Disabling Video Thumbnails + +Video thumbnails are enabled by default, but since they are a paid feature, they can also be disabled. + +When video thumbnails are enabled, the UI takes the following appearance: + +| Messages List (Light Mode) | Attachment Gallery (Light Mode) | +|---|---| +| ![Messages List Video Thumbs enabled Light Mode](../assets/configuration_message_list_video_thumbs_enabled.png) | ![Attachment Gallery Video Thumbs enabled Light Mode](../assets/configuration_attachment_gallery_video_thumbs_enabled.png) | + +| Messages List (Dark Mode) | Attachment Gallery (Dark Mode) | +|---|---| +| ![Messages List Video Thumbs enabled Dark Mode](../assets/configuration_message_list_video_thumbs_enabled_dark.png) | ![Attachment Gallery Video Thumbs enabled Dark Mode](../assets/configuration_attachment_gallery_video_thumbs_enabled_dark.png) | + + +You can disable the video thumbnails like so: + + + + +```kotlin +ChatUI.videoThumbnailsEnabled = false +``` + + + + +```java +ChatUI.setVideoThumbnailsEnabled(false); +``` + + + +Which makes the UI look like so: + +| Messages List (Light Mode) | Attachment Gallery (Light Mode) | +|---|---| +| ![Messages List Video Thumbs disabled Light Mode](../assets/configuration_message_list_video_thumbs_disabled.png) | ![Attachment Gallery Video Thumbs disabled Light Mode](../assets/configuration_attachment_gallery_video_thumbs_disabled.png) | + +| Messages List (Dark Mode) | Attachment Gallery (Dark Mode) | +|---|---| +| ![Messages List Video Thumbs disabled Dark Mode](../assets/configuration_message_list_video_thumbs_disabled_dark.png) | ![Attachment Gallery Video Thumbs disabled Dark Mode](../assets/configuration_attachment_gallery_video_thumbs_disabled_dark.png) | + +:::note +Only the video thumbnails fetched from Stream's APIs are paid and can be disabled. Video thumbnails loaded from local storage - such as the ones in the attachment picker - are not paid and will remain enabled. +::: + +## Customizing Message Decorators + +Message decorators are used to decorate the message UI elements. +For example, the `ReplyDecorator` is used to reflect `Message.replyTo` property on UI. + +You can customize the message decorators by overriding the default `ChatUI.decoratorProviderFactory`: + +### Removing Built-in Decorators + +For instance you can remove one the built-in decorators. +Let's try to remove the `ReplyDecorator`: + + + + +```kotlin +ChatUI.decoratorProviderFactory = DecoratorProviderFactory.defaultFactory { + it.type != Decorator.Type.BuiltIn.REPLY +} +``` + + + + +```java +ChatUI.setDecoratorProviderFactory( + DecoratorProviderFactory.defaultFactory( + decorator -> decorator.getType() != Decorator.Type.BuiltIn.REPLY + ) +); +``` + + + + +| DecoratorProviderFactory (default) | DecoratorProviderFactory (no ReplyDecorator) | +|--------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| ![Messages List Video Thumbs disabled Dark Mode](../assets/configuration_message_list_decorator_provider_default.png) | ![Attachment Gallery Video Thumbs disabled Dark Mode](../assets/configuration_message_list_decorator_provider_custom.png) | + +### Adding Custom Decorators +You can also add your own decorators. + +:::note +You can find the `ForwardedDecorator` related classes below in our [Sample App](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/decorator/). +::: + +Let's add a `ForwardedDecorator` that will display a `Forwarded` label for the **forwarded** messages: +```kotlin +// First, create a custom decorator type enum +enum class CustomDecoratorType : Decorator.Type { + FORWARDED, +} + +// Then, create a decorator +class ForwardedDecorator() : BaseDecorator() { + + override val type: Decorator.Type = CustomDecoratorType.FORWARDED + + private val forwardedViewId = View.generateViewId() + + override fun decorateCustomAttachmentsMessage( + viewHolder: CustomAttachmentsViewHolder, + data: MessageListItem.MessageItem, + ) { + setupForwardedView(viewHolder.binding.messageContainer, data) + } + + override fun decorateGiphyAttachmentMessage( + viewHolder: GiphyAttachmentViewHolder, + data: MessageListItem.MessageItem, + ) { + setupForwardedView(viewHolder.binding.messageContainer, data) + } + + override fun decorateFileAttachmentsMessage( + viewHolder: FileAttachmentsViewHolder, + data: MessageListItem.MessageItem, + ) { + setupForwardedView(viewHolder.binding.messageContainer, data) + } + + override fun decorateMediaAttachmentsMessage( + viewHolder: MediaAttachmentsViewHolder, + data: MessageListItem.MessageItem, + ) { + setupForwardedView(viewHolder.binding.messageContainer, data) + } + + override fun decoratePlainTextMessage(viewHolder: MessagePlainTextViewHolder, data: MessageListItem.MessageItem) { + setupForwardedView(viewHolder.binding.messageContainer, data, isPlainText = true) + } + + override fun decorateLinkAttachmentsMessage( + viewHolder: LinkAttachmentsViewHolder, + data: MessageListItem.MessageItem, + ) { + setupForwardedView(viewHolder.binding.messageContainer, data) + } + + private fun setupForwardedView( + container: ViewGroup, + data: MessageListItem.MessageItem, + isPlainText: Boolean = false, + ) { + // Check if the message is forwarded based on your custom message property + val isForwarded = data.message.extraData["forwarded"] as? Boolean ?: false + var textView = container.findViewById(forwardedViewId) + if (textView == null && isForwarded) { + textView = createTextView(container, isPlainText) + container.addView(textView, 0) + } + textView?.isVisible = isForwarded + } + + private fun createTextView(container: ViewGroup, isPlainText: Boolean) = TextView(container.context).apply { + id = forwardedViewId + layoutParams = MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + topMargin = Utils.dpToPx(MARGIN_TOP_DP) + marginStart = Utils.dpToPx(MARGIN_START_DP) + marginEnd = Utils.dpToPx(MARGIN_END_DP) + if (!isPlainText) bottomMargin = Utils.dpToPx(MARGIN_BOTTOM_DP) + } + setTextSize(TypedValue.COMPLEX_UNIT_SP, TEXT_SIZE) + setText(R.string.message_forwarded) + setTextColor(ContextCompat.getColor(container.context, R.color.message_forwarded)) + setCompoundDrawablesWithIntrinsicBounds(R.drawable.rounded_arrow_top_right_24, 0, 0, 0) + } + + companion object { + const val MARGIN_TOP_DP = 4 + const val MARGIN_START_DP = 8 + const val MARGIN_END_DP = 16 + const val MARGIN_BOTTOM_DP = 4 + + const val TEXT_SIZE = 13f + } +} +``` + +Then, add the decorator to the `CustomDecoratorProviderFactory` and `CustomDecoratorProvider`: +```kotlin +/** + * Custom decorator provider factory that creates a [CustomDecoratorProvider]. + */ +class CustomDecoratorProviderFactory : DecoratorProviderFactory { + override fun createDecoratorProvider( + channel: Channel, + dateFormatter: DateFormatter, + messageListViewStyle: MessageListViewStyle, + showAvatarPredicate: MessageListView.ShowAvatarPredicate, + messageBackgroundFactory: MessageBackgroundFactory, + deletedMessageVisibility: () -> DeletedMessageVisibility, + getLanguageDisplayName: (code: String) -> String, + ): DecoratorProvider = CustomDecoratorProvider( + channel, + dateFormatter, + messageListViewStyle, + showAvatarPredicate, + messageBackgroundFactory, + deletedMessageVisibility, + getLanguageDisplayName, + ) +} + +/** + * Custom decorator provider that creates the list of decorators. + */ +class CustomDecoratorProvider( + channel: Channel, + dateFormatter: DateFormatter, + messageListViewStyle: MessageListViewStyle, + showAvatarPredicate: MessageListView.ShowAvatarPredicate, + messageBackgroundFactory: MessageBackgroundFactory, + deletedMessageVisibility: () -> DeletedMessageVisibility, + getLanguageDisplayName: (code: String) -> String, +) : DecoratorProvider { + override val decorators by lazy { + // You can pass any of the above paparameters + // to your decorator to have more customized behavior. + listOf(ForwardedDecorator()) + } +} +``` + +Then you need to send the message with the `forwarded` property set to `true`. +You can do it by using the `extraData` property of the `Message` object: + +```kotlin +val channelClient = client.channel("messaging", "") + +// Create a forwarded message with the custom fields +val forwardedMessage = Message( + text = originalMessage.text, + extraData = mutableMapOf( + "forwarded" to true, + ), +) + +// Send the message to the channel +channelClient.sendMessage(message).enqueue { /* ... */ } +``` + +As a result, the forwarded message will be decorated with the custom decorator: + +| No ForwardedDecorator | Has ForwardedDecorator | +|-------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| ![Messages List Video Thumbs disabled Dark Mode](../assets/configuration_message_list_decorator_forwarded_disabled.png) | ![Attachment Gallery Video Thumbs disabled Dark Mode](../assets/configuration_message_list_decorator_forwarded_enabled.png) | + +On the **left** image you can see the default setup with `DecoratorProviderFactory.defaultFactory()` only: +```kotlin +ChatUI.decoratorProviderFactory = DecoratorProviderFactory.defaultFactory() +``` + +On the **right** image you can see the default setup along with the `ForwardedDecorator` added to the `CustomDecoratorProviderFactory`: +```kotlin +ChatUI.decoratorProviderFactory = CustomDecoratorProviderFactory() + DecoratorProviderFactory.defaultFactory() +``` + +## RTL Support + +UI Components support right-to-left (RTL) languages out of the box. The components can display content in a right-to-left direction and mirror associated UI elements. However, you may want to change the way certain components behave in RTL mode. + +### MessageComposerView configuration + +By default `MessageComposerView` determines the input direction based on the first strong directional character. To change the behavior you can override the center content slot of `MessageComposerView`. You can check how to override content slots [here](../message-composer/message-composer/#overriding-content-views). + +### MessageListView configuration + +To change RTL support for the messages in the `MessageListView` you can use custom view holders with a different text direction. You can check how to implement custom view holders [here](../message-list/message-list/#custom-message-views). + +### ChannelListView configuration + +To change RTL support for the channels in the `ChannelListView` you can use custom view holders with a different text direction. You can check how to implement custom view holders [here](../channel-list/channel-list/#creating-a-custom-viewholder-factory). diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/01-channel-list-screen.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/01-channel-list-screen.mdx new file mode 100644 index 00000000000..d331a14a893 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/01-channel-list-screen.mdx @@ -0,0 +1,350 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Channel List Screen + +The easiest way to set up a screen that shows the active user's channels and give them the ability to search for a specific channel is to use one of the following components: + +* `ChannelListFragment`: A Fragment that represents a self-contained channel list screen. +* `ChannelListActivity`: An Activity that is just a thin wrapper around `ChannelListFragment`. + +The `ChannelListFragment` contains these four inner components: + +* [`ChannelListHeaderView`](02-channel-list-header.mdx): Displays information about the current user and the connection state. +* [`ChannelListView`](03-channel-list.mdx): Displays a list of channel items in a paginated list. +* [`SearchInputView`](../05-message-list/04-search-view.mdx): An input field to search for messages that contain specific text. +* [`SearchResultListView`](../05-message-list/04-search-view.mdx): Displays a list of search result items. + +:::note +Fragments and Activities representing self-contained screens are easy to use. They allow you to explore the SDK's features in a breeze, however, they offer limited customization. +::: + +## Usage + +To use the channel list screen, simply start `ChannelListActivity` from the SDK: + + + + +```kotlin +context.startActivity(ChannelListActivity.createIntent(context)) +``` + + + + +```java +context.startActivity(ChannelListActivity.createIntent(context)); +``` + + + +This single line of code will produce a fully working solution, as shown in the image below. + +|![The Channel List Screen Component](../../assets/channel_list_screen.png)| +|---| + +Alternatively, you can achieve the same result by adding `ChannelListFragment` from the SDK to your Fragment or Activity: + +```xml + + +``` + + + + +```kotlin +class MyChannelListActivity : AppCompatActivity(R.layout.fragment_container) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, ChannelListFragment.newInstance()) + .commit() + } + } +} +``` + + + + +```java +public final class MyChannelListActivity extends AppCompatActivity { + + public MyChannelListActivity() { + super(R.layout.fragment_container); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, ChannelListFragment.newInstance()) + .commit(); + } + } +} +``` + + + +Next, let's see how to handle actions on the screen. + +## Handling Actions + +To handle actions supported by `ChannelListFragment` you have to implement corresponding click listeners in the parent Fragment or Activity: + + + + +```kotlin +class MyChannelListActivity : AppCompatActivity(R.layout.fragment_container), + ChannelListFragment.HeaderActionButtonClickListener, + ChannelListFragment.HeaderUserAvatarClickListener, + ChannelListFragment.ChannelListItemClickListener, + ChannelListFragment.SearchResultClickListener { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Add ChannelListFragment to the layout + } + + override fun onUserAvatarClick() { + // Handle header avatar click + } + + override fun onActionButtonClick() { + // Handle header action button click + } + + override fun onChannelClick(channel: Channel) { + // Handle channel click + } + + override fun onSearchResultClick(message: Message) { + // Handle search result click + } +} +``` + + + + +```java +public final class MyChannelListActivity extends AppCompatActivity implements + ChannelListFragment.HeaderActionButtonClickListener, + ChannelListFragment.HeaderUserAvatarClickListener, + ChannelListFragment.ChannelListItemClickListener, + ChannelListFragment.SearchResultClickListener { + + public MyChannelListActivity() { + super(R.layout.fragment_container); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Add ChannelListFragment to the layout + } + + @Override + public void onUserAvatarClick() { + // Handle header avatar click + } + + @Override + public void onActionButtonClick() { + // Handle header action button click + } + + @Override + public void onChannelClick(@NonNull Channel channel) { + // Handle channel click + } + + @Override + public void onSearchResultClick(@NonNull Message message) { + // Handle search result click + } +} +``` + + + +These are the main click listeners you can use with the `ChannelListFragment`: + +* `HeaderActionButtonClickListener`: Click listener for the right button in the header. Not implemented by default. +* `HeaderUserAvatarClickListener`: Click listener for the left button in the header represented by the avatar of the current user. Not implemented by default. +* `ChannelListItemClickListener`: Click listener for channel item clicks. Navigates to `MessageListActivity` by default. +* `SearchResultClickListener`: Click listener for search result items. Navigates to `MessageListActivity` by default. + +## Customization + +The channel list screen component offers limited customization. The `ChannelListFragment` exposes a builder with the following methods: + +* `setFragment`: Sets a custom channel list Fragment. The Fragment must be a subclass of `ChannelListFragment`. +* `customTheme`: Custom theme for the screen. +* `showHeader`: Whether the header is shown or hidden. +* `showSearch`: Whether the search input is shown or hidden. +* `headerTitle`: Header title. "Stream Chat" by default. + +Other than that, you can use inheritance for further customization as shown in the example below: + + + + +```kotlin +class CustomChannelListFragment : ChannelListFragment() { + + override fun setupChannelListHeader(channelListHeaderView: ChannelListHeaderView) { + super.setupChannelListHeader(channelListHeaderView) + // Customize channel list header view + + // For example, set a custom listener for the avatar + channelListHeaderView.setOnUserAvatarClickListener { + // Handle avatar click + } + } + + override fun setupChannelList(channelListView: ChannelListView) { + super.setupChannelList(channelListView) + // Customize channel list view + } + + override fun setupSearchInput(searchInputView: SearchInputView) { + super.setupSearchInput(searchInputView) + // Customize search input field + } + + override fun setupSearchResultList(searchResultListView: SearchResultListView) { + super.setupSearchResultList(searchResultListView) + // Customize search result list + } + + override fun getFilter(): FilterObject? { + // Provide custom filter + return null + } + + override fun getSort(): QuerySorter { + // Provide custom sort + return super.getSort() + } +} + +class CustomChannelListActivity : ChannelListActivity() { + + override fun createChannelListFragment(): ChannelListFragment { + return ChannelListFragment.newInstance { + setFragment(CustomChannelListFragment()) + customTheme(R.style.StreamUiTheme) + showSearch(true) + showHeader(true) + headerTitle("Title") + } + } +} +``` + + + + +```java +public final class CustomChannelListFragment extends ChannelListFragment { + + @Override + protected void setupChannelListHeader(@NonNull ChannelListHeaderView channelListHeaderView) { + super.setupChannelListHeader(channelListHeaderView); + // Customize channel list header view + + // For example, set a custom listener for the avatar + channelListHeaderView.setOnUserAvatarClickListener(() -> { + // Handle avatar click + }); + } + + @Override + protected void setupChannelList(@NonNull ChannelListView channelListView) { + super.setupChannelList(channelListView); + // Customize channel list view + } + + @Override + protected void setupSearchInput(@NonNull SearchInputView searchInputView) { + super.setupSearchInput(searchInputView); + // Customize search input field + } + + @Override + protected void setupSearchResultList(@NonNull SearchResultListView searchResultListView) { + super.setupSearchResultList(searchResultListView); + // Customize search result list + } + + @Nullable + @Override + protected FilterObject getFilter() { + // Provide custom filter + return super.getFilter(); + } + + @NonNull + @Override + protected QuerySorter getSort() { + // Provide custom sort + return super.getSort(); + } +} + +public final class CustomChannelListActivity extends ChannelListActivity { + + @NonNull + @Override + protected ChannelListFragment createChannelListFragment() { + return ChannelListFragment.newInstance(builder -> { + builder.setFragment(new CustomChannelListFragment()); + builder.customTheme(R.style.StreamUiTheme); + builder.showSearch(true); + builder.showHeader(true); + builder.headerTitle("Title"); + return Unit.INSTANCE; + }); + } +} +``` + + + +Then you need to add `CustomChannelListActivity` to your `AndroidManifest.xml`, create an Intent for it using the `ChannelListActivity.createIntent()` method, and finally start the Activity: + + + + +```kotlin +context.startActivity( + ChannelListActivity.createIntent( + context = context, + activityClass = CustomChannelListActivity::class.java + ) +) +``` + + + + +```java +context.startActivity(ChannelListActivity.createIntent(context, CustomChannelListActivity.class)); +``` + + + +:::note +Fragments and Activities representing self-contained screens can be styled using the options described in the [theming](02-ui-components/02-theming.mdx) guide. +::: diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/02-channel-list-header.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/02-channel-list-header.mdx new file mode 100644 index 00000000000..dece371d189 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/02-channel-list-header.mdx @@ -0,0 +1,109 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Channel List Header + +`ChannelListHeaderView` is a component that shows the title of the channels list, the current connection status, the avatar of the current user, and provides an action button that can be used to create a new conversation. It is designed to be displayed at the top of the channels screen of your app. + + | Light Mode | Dark Mode | + |-------------------------------------------------|-----------------------------------------------------| + | ![Light_mode](../../assets/channels_header.png) | ![Dark_mode](../../assets/channels_header_dark.png) | + +## Usage + +To use `ChannelListHeaderView`, include it in your XML layout as shown below: + +```xml + +``` + +`ChannelListHeaderView` is supposed to work with `ChannelListHeaderViewModel`. The basic setup of the ViewModel and connecting it with the View can be done in the following way: + + + + + ```kotlin +// Instantiate the ViewModel +val viewModel: ChannelListHeaderViewModel by viewModels() + +// Bind the ViewModel with ChannelListHeaderView +viewModel.bindView(channelListHeaderView, viewLifecycleOwner) +``` + + + + +```java +// Instantiate the ViewModel +ChannelListHeaderViewModel viewModel = new ViewModelProvider(this).get(ChannelListHeaderViewModel.class); + +// Bind it with ChannelListHeaderView +ChannelListHeaderViewModelBinding.bind(viewModel, channelListHeaderView, getViewLifecycleOwner()); +``` + + + +The `ChannelListHeaderViewModel::bindView` function provides all the logic of subscribing to data emitted by the ViewModel. By default, the ViewModel will make the View display the avatar of the currently logged-in user as well as the "Waiting for network" state when needed. + + | Light Mode | Dark Mode | + |---------------------------------------------------------------------|-------------------------------------------------------------------------| + | ![Light_mode](../../assets/channels_header_waiting_for_network.png) | ![Dark_mode](../../assets/channels_header_waiting_for_network_dark.png) | + +## Handling Actions + +The View displays an avatar and action button by default. You can set listeners to handle when a user clicks these: + + + + +```kotlin +channelListHeaderView.setOnActionButtonClickListener { + // Handle action button click +} +channelListHeaderView.setOnUserAvatarClickListener { + // Handle user avatar click +} +``` + + + + +```java +channelListHeaderView.setOnActionButtonClickListener(() -> { + // Handle action button click +}); +channelListHeaderView.setOnUserAvatarClickListener(() -> { + // Handle user avatar click +}); +``` + + + +You can also use XML attributes to hide those Views instead. This is explained below. + +## Customization + +### Using XML Attributes + +The appearance of `ChannelListHeaderView` can be conveniently modified using its XML attributes. +```xml + +``` + +The example above hides the avatar view, makes the title text bold and sets the drawable of the action button to a custom icon. + +| Before | After | +|-------------------------------------------------|--------------------------------------------------------------------| +| ![Light_mode](../../assets/channels_header.png) | ![Dark_mode](../../assets/channels_header_after_customization.png) | + +A full list of available XML attributes is available [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_channel_list_header_view.xml). diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/03-channel-list.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/03-channel-list.mdx new file mode 100644 index 00000000000..52c1bfc7893 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/03-channel-list.mdx @@ -0,0 +1,639 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# ChannelListView + +`ChannelListView` is a component that displays a list of channels available to the user. For users with a slower connection or that don't belong to any channels yet, `ChannelListView` also supports loading and empty states. + +| Light Mode | Dark Mode | +| --- | --- | +|![Light_mode](../../assets/channel_list_view_component_swipe_actions.png)| ![Dark_mode](../../assets/channel_list_view_component_swipe_actions_dark.png) | + +By default, a single channel item shows the following: + +* Channel name +* User's read state +* Latest message +* Timestamp of the latest message + +It also supports swiping behaviour which allows handling different channel actions. + +## Usage + +To use `ChannelListView`, include it in your XML layout as shown below: + +```xml + +``` + +We recommend fetching data from Stream's API using `ChanneListViewModel`, and then rendering it inside `ChannelListView`. + +The basic setup of the ViewModel and connecting it to the View is done the following way: + + + + +```kotlin +// Instantiate the ViewModel +val viewModel: ChannelListViewModel by viewModels { + ChannelListViewModelFactory( + filter = Filters.and( + Filters.eq("type", "messaging"), + Filters.`in`("members", listOf(ChatClient.instance().getCurrentUser()!!.id)), + ), + sort = QuerySortByField.descByName("last_updated"), + limit = 30, + ) +} +// Bind the ViewModel with ChannelListView +viewModel.bindView(channelListView, viewLifecycleOwner) +``` + + + + +```java +// Instantiate the ViewModel +FilterObject filter = Filters.and( + Filters.eq("type", "messaging"), + Filters.in("members", Collections.singletonList(ChatClient.instance().getCurrentUser().getId())) +); + +ViewModelProvider.Factory factory = new ChannelListViewModelFactory.Builder() + .filter(filter) + .sort(QuerySortByField.descByName("last_updated")) + .limit(30) + .build(); + +ChannelListViewModel viewModel = new ViewModelProvider(this, factory).get(ChannelListViewModel.class); + +// Bind the ViewModel with ChannelListView +ChannelListViewModelBinding.bind(viewModel, channelListView, getViewLifecycleOwner()); +``` + + + +:::note + +You may need to pass a custom `ChatEventHandler` to make sure the list is updated properly. Check [ChannelListUpdates](../../08-client/06-guides/05-channel-list-updates.mdx) section to read more. +::: + +All the logic of subscribing to data emitted by the ViewModel is provided by the `bindView` function. Other than channel data loading, the ViewModel also handles actions like deleting a channel and leaving a group conversation by default. + +## Handling Actions + +`ChannelListView` includes a set of channel actions. Actions on `ChannelListView` items are available on swipe. With these, you can: + +* Delete the channel if you have sufficient permissions +* See channel members +* Leave the channel if it's a group channel + +| Light Mode | Dark Mode | +| --- | --- | +|![Light_mode](../../assets/channel_action_light.png)|![Dark_mode](../../assets/channel_action_dark.png)| + +The following actions are not implemented by default, so you should add your own listeners if you want to handle them: + + + + +```kotlin +channelListView.setChannelItemClickListener { channel -> + // Handle channel click +} +channelListView.setChannelInfoClickListener { channel -> + // Handle channel info click +} +channelListView.setChannelLongClickListener { channel -> + // Handle channel long click +} +channelListView.setChannelListUpdateListener { channels -> + // Handle channel list updates +} +channelListView.setUserClickListener { user -> + // Handle member click +} +``` + + + + +```java +channelListView.setChannelItemClickListener(channel -> { + // Handle channel click +}); +channelListView.setChannelInfoClickListener(channel -> { + // Handle channel info click +}); +channelListView.setChannelLongClickListener(channel -> { + // Handle channel info click +}); +channelListView.setChannelListUpdateListener(channels -> { + // Handle channel list updates +}); +channelListView.setUserClickListener(user -> { + // Handle member click +}); +``` + + + +The full list of available listeners is available [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.channels.list/-channel-list-view/). + +## Customization + +There are two ways to customize the appearance of `ChannelListView`: + +* Using XML attributes +* Using the `TransformStyle` API at runtime to customize the style of all `ChannelListView` instances + +### Using XML Attributes + +There are many XML attributes that can be used to customize the appearance of the channel list. The most useful ones include: + +* `streamUiChannelDeleteEnabled`: Whether the delete icon should be displayed. +* `streamUiChannelDeleteIcon`: Drawable reference for the channel delete icon. +* `streamUiChannelTitleTextColor`: Color of the channel title text. +* `streamUiChannelTitleTextSize`: Size of the channel title text. +* `streamUiLastMessageTextSize`: Size of the last message text. +* `streamUiLastMessageTextColor`: Color of the last message text. +* `streamUiForegroundLayoutColor`: Foreground color of the channel list item. +* `streamUiBackgroundLayoutColor`: Background color of the channel list item, visible when swiping the list item. + +The full list of available XML attributes is available under `ChannelListView` styleable, [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_channel_list_view.xml). + +### Using Style Transformations + +The following example shows how to modify the style of all `ChannelListView` instances globally to: + +* Disable the default options +* Change the foreground color +* Change the read indicator icon +* Change the title text style +* Change the background color for unread messages + +To make these changes, we need to define a custom `TransformStyle.channelListStyleTransformer`: + + + + +```kotlin +TransformStyle.channelListStyleTransformer = StyleTransformer { defaultStyle -> + defaultStyle.copy( + optionsEnabled = false, + foregroundLayoutColor = Color.LTGRAY, + indicatorReadIcon = ContextCompat.getDrawable(context, R.drawable.stream_ui_ic_flag)!!, + channelTitleText = defaultStyle.channelTitleText.copy( + color = Color.BLUE, + size = context.resources.getDimensionPixelSize(R.dimen.stream_ui_text_large), + ), + unreadMessageCounterBackgroundColor = Color.BLUE, + ) +} +``` + + + + +```java +TransformStyle.setChannelListStyleTransformer(source -> { + // Customize the style + return source; +}); +``` + + + +These changes should have the following results: + +| Before | After | +| --- | --- | +|![before](../../assets/channel_list_view_style_before.png)|![after](../../assets/channel_list_view_style_after.png)| + +:::note +The transformer should be set before the View is rendered to make sure that the new style was applied. +::: + +## Customizing Swipe Actions + +By default, `ChannelListView` supports **two** swipe actions: +* `More Options` - available for every channel. +* `Delete` - available for the channels where `Channel.ownCapbilities` contains `"delete-channel"` + +Here is the **default** implementation: + +| Swipe Actions | +|-----------------------------------------------------------| +| ![before](../../assets/xml_customizing_swipe_actions.png) | + +:::note +You can customize the swipe actions as shown below. +::: + +### Customizing Icons + + + +```kotlin +channelsView.setMoreOptionsIconProvider { channel -> + // You can generate the icon Drawable based on the channel object + ContextCompat.getDrawable(context, R.drawable.custom_action_icon_more) +} +channelsView.setDeleteOptionIconProvider { channel -> + // You can generate the icon Drawable based on the channel object + ContextCompat.getDrawable(context, R.drawable.custom_action_icon_delete) +} +``` + + + + +```java +channelsView.setDeleteOptionIconProvider(channel -> { + // You can generate icon Drawable based on the channel object + return ContextCompat.getDrawable(context, R.drawable.custom_action_icon_more); +}); +channelsView.setDeleteOptionIconProvider(channel -> { + // You can generate icon Drawable based on the channel object + return ContextCompat.getDrawable(context, R.drawable.custom_action_icon_delete); +}); +``` + + + +### Customizing Visibility + + + +```kotlin +channelsView.setIsMoreOptionsVisible { channel -> + // You can determine visibility based on the channel object. + true +} +channelsView.setIsDeleteOptionVisible { channel -> + // You can determine visibility based on the channel object. + // Here is the default implementation: + channel.ownCapabilities.contains("delete-channel") +} +``` + + + + +```java +channelsView.setIsMoreOptionsVisible(channel -> { + // you generate icon based on the channel object + return true; +}); +channelsView.setIsDeleteOptionVisible(channel -> { + // You can determine visibility based on the channel object. + // Here is the default implementation: + return channel.getOwnCapabilities().contains("delete-channel"); +}); +``` + + + +## Creating a Custom ViewHolder Factory + +`ChannelListView` provides a way to completely change the default ViewHolders and add different types of views. All you need to do is to provide your own `ChannelListItemViewHolderFactory`. Let's see an example that displays the channel's photo, name, and member count. + +1. Create the `custom_channel_list_item.xml` layout: + +```xml + + + + + + + + + + +``` + +2. Add this _plurals_ entry to `strings.xml`: + +```xml + + %1d Member + %1d Members + +``` + +3. Create a custom ViewHolder and ViewHolder factory: + + + + +```kotlin +class CustomChannelListItemViewHolderFactory : ChannelListItemViewHolderFactory() { + override fun createChannelViewHolder(parentView: ViewGroup): BaseChannelListItemViewHolder { + return CustomChannelViewHolder(parentView, listenerContainer.channelClickListener) + } +} + +class CustomChannelViewHolder( + parent: ViewGroup, + private val channelClickListener: ChannelListView.ChannelClickListener, + private val binding: CustomChannelListItemBinding = CustomChannelListItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), +) : BaseChannelListItemViewHolder(binding.root) { + + private lateinit var channel: Channel + + init { + binding.root.setOnClickListener { channelClickListener.onClick(channel) } + } + + override fun bind(channel: Channel, diff: ChannelListPayloadDiff) { + this.channel = channel + + binding.channelAvatarView.setChannel(channel) + binding.channelNameTextView.text = ChatUI.channelNameFormatter.formatChannelName( + channel = channel, + currentUser = ChatClient.instance().getCurrentUser() + ) + binding.membersCountTextView.text = itemView.context.resources.getQuantityString( + R.plurals.members_count, + channel.members.size, + channel.members.size + ) + } +} +``` + + + + +```java +public class CustomChannelListItemViewHolderFactory extends ChannelListItemViewHolderFactory { + @NonNull + @Override + protected BaseChannelListItemViewHolder createChannelViewHolder(@NonNull ViewGroup parent) { + CustomChannelListItemBinding binding = CustomChannelListItemBinding + .inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new CustomChannelViewHolder(binding, getListenerContainer().getChannelClickListener()); + } +} + +public class CustomChannelViewHolder extends BaseChannelListItemViewHolder { + + private CustomChannelListItemBinding binding; + + private Channel channel; + + public CustomChannelViewHolder(CustomChannelListItemBinding binding, + ChannelListView.ChannelClickListener channelClickListener) { + super(binding.getRoot()); + this.binding = binding; + + binding.getRoot().setOnClickListener(v -> channelClickListener.onClick(channel)); + } + + @Override + public void bind(@NonNull Channel channel, @NonNull ChannelListPayloadDiff diff) { + this.channel = channel; + binding.channelAvatarView.setChannel(channel); + String channelName = ChatUI.getChannelNameFormatter().formatChannelName( + channel, + ChatClient.instance().getCurrentUser() + ); + binding.channelNameTextView.setText(channelName); + String memberCount = itemView.getContext().getResources().getQuantityString( + R.plurals.members_count, + channel.getMembers().size(), + channel.getMembers().size() + ); + binding.membersCountTextView.setText(memberCount); + } +} +``` + + + +4. Set the custom ViewHolder factory on the `ChannelListView`: + + + + +```kotlin +// Create custom view holder factory +val customFactory = CustomChannelListItemViewHolderFactory() + +// Set custom view holder factory +channelListView.setViewHolderFactory(customFactory) +``` + + + + +```java +// Create custom view holder factory +CustomChannelListItemViewHolderFactory customFactory = new CustomChannelListItemViewHolderFactory(); + +// Set custom view holder factory +channelListView.setViewHolderFactory(customFactory); +``` + + + +These changes should have the following results: + +|![Custom ViewHolder](../../assets/custom_view_holder.png)| +|---| + +## Creating a Custom Loading View + +A custom loading view can be set using the `setLoadingView` method. Assuming that we have a setup similar to the one seen in the previous steps, we can create a loading view with a shimmer effect by taking the following actions: + +1. Add the Shimmer dependency in your `build.gradle` file's dependencies block: + +```groovy +implementation "com.facebook.shimmer:shimmer:0.5.0" +``` + +2. Add `shape_shimmer.xml` into _drawable_ folder: + +```xml + + + + + +``` + +3. Add a single row layout - `item_loading_view.xml` into _layout_ folder: + +```xml + + + + + + + + + + + + + + +``` + +4. Create the final loading view with shimmer effect. Let's call it `channel_list_loading_view`: + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +5. Change `ChannelListView`'s loading view: + + + + +```kotlin +// Inflate loading view +val loadingView = LayoutInflater.from(context).inflate(R.layout.channel_list_loading_view, null) +// Set loading view +channelListView.setLoadingView(loadingView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) +``` + + + + +```java +// Inflate loading view +View loadingView = LayoutInflater.from(getContext()).inflate(R.layout.channel_list_loading_view, null); +// Set loading view +channelListView.setLoadingView(loadingView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); +``` + + + +These changes should have the following results: + +|![Custom Loading View](../../assets/channel_list_shimmer.png)| +|---| diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/_category_.json b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/_category_.json new file mode 100644 index 00000000000..72bcd44f527 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/04-channel-list/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Channel List" +} diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/01-message-list-screen.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/01-message-list-screen.mdx new file mode 100644 index 00000000000..c279fa43499 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/01-message-list-screen.mdx @@ -0,0 +1,281 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Message List Screen + +You can set up a self-contained chat screen that displays a list of messages and gives users the ability to send messages by using one of the following components: + +* `MessageListFragment`: A Fragment that represents a self-contained chat screen. +* `MessageListActivity`: An Activity that is just a thin wrapper around `MessageListFragment`. + +The `MessageListFragment` contains these three inner components: + +* [`MessageListHeaderView`](02-message-list-header.mdx): Displays a navigation icon, the name of the channel or thread and the channel avatar. +* [`MessageListView`](03-message-list.mdx): Shows a list of paginated messages, with threads, replies, reactions and deleted messages. +* [`MessageComposerView`](../06-message-composer/01-message-composer.mdx): Allows users to participate in the chat by sending messages and attachments. + +:::note +Fragments and Activities representing self-contained screens are easy to use. They allow you to explore the features of our SDK in a breeze, however, they offer limited customization. +::: + +## Usage + +To use the message list screen, you can simply start `MessageListActivity` from the SDK: + + + + +```kotlin +context.startActivity(MessageListActivity.createIntent(context, cid = "messaging:123")) +``` + + + + +```java +context.startActivity(MessageListActivity.createIntent(context, "messaging:123")); +``` + + + +This single line of code will produce a fully working solution, as shown in the image below. + +|![The Message List Screen Component](../../assets/message_list_screen.png)| +|---| + +Alternatively, you can achieve the same result by adding `MessageListFragment` from the SDK to your Fragment or Activity: + +```xml + + +``` + + + + +```kotlin +class MyMessageListActivity : AppCompatActivity(R.layout.fragment_container) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val fragment = MessageListFragment.newInstance(cid = "messaging:123") { + showHeader(true) + } + supportFragmentManager.beginTransaction() + .replace(R.id.container, fragment) + .commit() + } + } +} +``` + + + + +```java +public final class MyMessageListActivity extends AppCompatActivity { + + public MyMessageListActivity() { + super(R.layout.fragment_container); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + MessageListFragment fragment = MessageListFragment.newInstance("messaging:123", builder -> { + builder.showHeader(true); + return Unit.INSTANCE; + }); + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, fragment) + .commit(); + } + } +} +``` + + + +Next, let's see how to handle actions on the screen. + +## Handling Actions + +To handle actions supported by `MessageListFragment` you have to implement corresponding click listeners in the parent Fragment or Activity: + + + + +```kotlin +class MyMessageListActivity : AppCompatActivity(R.layout.fragment_container), MessageListFragment.BackPressListener { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Add MessageListFragment to the layout + } + + override fun onBackPress() { + // Handle back press + } +} +``` + + + + +```java +public final class MyMessageListActivity extends AppCompatActivity implements MessageListFragment.BackPressListener { + + public MyMessageListActivity() { + super(R.layout.fragment_container); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Add MessageListFragment to the layout + } + + @Override + public void onBackPress() { + // Handle back press + } +} +``` + + + +Currently, there's only a single click listener you can use with the `MessageListFragment`: + +* `BackPressListener`: Click listener for the navigation button in the header. Finishes Activity by default. + +## Customization + +The message list screen component offers limited customization. The `MessageListFragment` exposes a builder with the following methods: + +* `setFragment`: Sets custom message list Fragment. The Fragment must be a subclass of `MessageListFragment`. +* `customTheme`: Custom theme for the screen. +* `showHeader`: Whether the header is shown or hidden. +* `messageId`: The ID of the message to highlight. + +Other than that, you can use inheritance for further customization as shown in the example below: + + + + +```kotlin +class CustomMessageListFragment : MessageListFragment() { + + override fun setupMessageListHeader(messageListHeaderView: MessageListHeaderView) { + super.setupMessageListHeader(messageListHeaderView) + // Customize message list header view + + // For example, set a custom listener for the back button + messageListHeaderView.setBackButtonClickListener { + // Handle back press + } + } + + override fun setupMessageList(messageListView: MessageListView) { + super.setupMessageList(messageListView) + // Customize message list view + } + + override fun setupMessageComposer(messageComposerView: MessageComposerView) { + super.setupMessageComposer(messageComposerView) + // Customize message composer view + } +} + +class CustomMessageListActivity : MessageListActivity() { + + override fun createMessageListFragment(cid: String, messageId: String?): MessageListFragment { + return MessageListFragment.newInstance(cid) { + setFragment(CustomMessageListFragment()) + customTheme(R.style.StreamUiTheme) + showHeader(true) + messageId(messageId) + } + } +} +``` + + + + +```java +public final class CustomMessageListFragment extends MessageListFragment { + + @Override + protected void setupMessageListHeader(@NonNull MessageListHeaderView messageListHeaderView) { + super.setupMessageListHeader(messageListHeaderView); + // Customize message list header view + + // For example, set a custom listener for the back button + messageListHeaderView.setBackButtonClickListener(() -> { + // Handle back press + }); + } + + @Override + protected void setupMessageList(@NonNull MessageListView messageListView) { + super.setupMessageList(messageListView); + // Customize message list view + } + + @Override + protected void setupMessageComposer(@NonNull MessageComposerView messageComposerView) { + super.setupMessageComposer(messageComposerView); + // Customize message composer view + } +} + +public final class CustomMessageListActivity extends MessageListActivity { + + @NonNull + @Override + protected MessageListFragment createMessageListFragment(@NonNull String cid, @Nullable String messageId) { + return MessageListFragment.newInstance(cid, builder -> { + builder.setFragment(new CustomMessageListFragment()); + builder.customTheme(R.style.StreamUiTheme); + builder.showHeader(true); + builder.messageId(messageId); + return Unit.INSTANCE; + }); + } +} +``` + + + +Then you need to add `CustomMessageListActivity` to your `AndroidManifest.xml`, create an Intent for it using the `MessageListActivity.createIntent()` method, and finally start the Activity: + + + + +```kotlin +context.startActivity( + MessageListActivity.createIntent( + context = context, + cid = "messaging:123", + activityClass = CustomMessageListActivity::class.java + ) +) +``` + + + + +```java +context.startActivity(MessageListActivity.createIntent(context, "messaging:123", null, CustomMessageListActivity.class)); +``` + + + +:::note +Fragments and Activities representing self-contained screens can be styled using the options described in the [theming](../02-theming.mdx) guide. +::: diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/02-message-list-header.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/02-message-list-header.mdx new file mode 100644 index 00000000000..96948e0cd7f --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/02-message-list-header.mdx @@ -0,0 +1,126 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Message List Header + +`MessageListHeaderView` is a component that can be used on a message list screen. It shows the channel's name and avatar, members and online members count, current connection status, and a back button. + +| Light Mode | Dark Mode | +| --- | --- | +|![Light_mode](../../assets/message_list_header.png)|![Dark_mode](../../assets/message_list_header_dark.png)| + +## Usage + +To use `MessageListHeaderView`, include it in your XML layout as shown below: + +```xml + +``` + +We recommend using the `MessageListHeaderViewModel` that gets all needed data from the Stream API and then renders it in the view. + +The basic setup of the ViewModel and connecting it to the view is done the following way: + + + + +```kotlin +// Initialize ViewModel +val viewModel: MessageListHeaderViewModel by viewModels { + MessageListViewModelFactory(cid = "messaging:123") +} + +// Bind the View and ViewModel +viewModel.bindView(messageListHeaderView, lifecycleOwner) +``` + + + + +```java +// Initialize ViewModel +ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder() + .cid("messaging:123") + .build(); +ViewModelProvider provider = new ViewModelProvider(this, factory); +MessageListHeaderViewModel viewModel = provider.get(MessageListHeaderViewModel.class); + +// Bind the View and ViewModel +MessageListHeaderViewModelBinding.bind(viewModel, messageListHeaderView, getViewLifecycleOwner()); +``` + + + +By default, the ViewModel will make the View display useful channel information and the "Searching for network" state when needed. + + | Light Mode | Dark Mode | + | --- | --- | + |![Light_mode](../../assets/message_list_header_waiting_for_network.png)|![Dark_mode](../../assets/message_list_header_waiting_for_network_dark.png)| + +## Handling Actions + +By default, `MessageListHeaderView` displays all the Views described above, but none of them come with a default click behavior. You can change that by setting the following listeners: + + + + +```kotlin +messageListHeaderView.setBackButtonClickListener { + // Handle back button click +} +messageListHeaderView.setAvatarClickListener { + // Handle avatar click +} +messageListHeaderView.setTitleClickListener { + // Handle title click +} +messageListHeaderView.setSubtitleClickListener { + // Handle subtitle click +} +``` + + + + +```java +messageListHeaderView.setBackButtonClickListener(() -> { + // Handle back button click +}); +messageListHeaderView.setAvatarClickListener(() -> { + // Handle avatar click +}); +messageListHeaderView.setTitleClickListener(() -> { + // Handle title click +}); +messageListHeaderView.setSubtitleClickListener(() -> { + // Handle subtitle click +}); +``` + + + +## Customization + +### Using XML Attributes + +The appearance of `MessageListHeaderView` can be conveniently modified using its XML attributes. +```xml + +``` + +The example above hides the back button, makes the title text red and subtitle text bold. + +| Before | After | +| --- | --- | +|![Light_mode](../../assets/message_list_header.png)|![Dark_mode](../../assets/message_list_header_customization.png)| + +A full list of available XML attributes is available [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_header_view.xml). diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/03-message-list.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/03-message-list.mdx new file mode 100644 index 00000000000..4bc41b50dd6 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/03-message-list.mdx @@ -0,0 +1,818 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# MessageListView + +`MessageListView` is one of our core UI components, which displays a list of messages for the given channel. It contains the following list of possible items: + +- Plain text message +- Text message with attachments (media or file) +- Deleted message (depending on the set `DeletedMessageVisibility` value) +- Error message (for example inappropriate messages blocked by moderation) +- System message (for example when a user joins the channel) +- Giphy preview +- Date separator +- Loading more indicator +- Thread separator (for thread mode only) +- Typing indicator + +You're able to customize the appearance of this component using custom attributes as well as method calls at runtime. `MessageListView` also contains a set of overridable action/option handlers and event listeners. By default, this component has the following appearance: + +| Light Mode | Dark Mode | +| --- | --- | +|![Message list overview in light mode](../../assets/message_list_view_overview_light.png)|![Message list overview in dark mode](../../assets/message_list_view_overview_dark.png)| + +## Usage + +If you want to keep the default design and behavior of this component then getting started with it is very simple: + +1. Add the component to your XML layout hierarchy. +2. Bind it to a `MessageListViewModel`. + +Adding `MessageListView` to your layout is as easy as inserting the following lines to your layout hierarchy: + +```xml + +``` + +The UI components library includes a `ViewModel` for `MessageListView`.
+Binding it to the `View` is easily accomplished using an extension function called `bindView`: + + + + +```kotlin +// 1. Init ViewModel +val viewModel: MessageListViewModel by viewModels { + MessageListViewModelFactory(cid = "messaging:123") +} + +// 2. Bind View and ViewModel +viewModel.bindView(messageListView, lifecycleOwner) +``` + + + + +```java +// Init ViewModel +ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder() + .cid("messaging:123") + .build(); +ViewModelProvider provider = new ViewModelProvider(this, factory); +MessageListViewModel viewModel = provider.get(MessageListViewModel.class); + +// Bind View and ViewModel +MessageListViewModelBinding.bind(viewModel, messageListView, getViewLifecycleOwner()); +``` + + + +## Handling Actions + +`MessageListView` contains a set of actions which are activated by long pressing a message: + +* Adding reactions +* Replying +* Replying in a thread +* Copying the message +* Editing the message +* Deleting the message +* Flagging the message + +:::note +Certain actions are subject to how user permissions are setup. You can find them inside the [Stream Dashboard](https://dashboard.getstream.io).
+Learn more about permissions [here](https://getstream.io/chat/docs/other-rest/chat_permission_policies/). +::: + +| Light Mode | Dark Mode | +| --- | --- | +|![Message options in light mode](../../assets/message_options_light.png)|![Message options in dark mode](../../assets/message_options_dark.png)| + +Default action handlers are set up when binding the ViewModel to the View.
+You can customize the default behavior by overriding each of the following handlers: + + + + +```kotlin +messageListView.setLastMessageReadHandler { + // Handle when last message got read +} +messageListView.setEndRegionReachedHandler { + // Handle when end region reached +} +messageListView.setMessageDeleteHandler { message: Message -> + // Handle when message is going to be deleted +} +messageListView.setThreadStartHandler { message: Message -> + // Handle when new thread for message is started +} +messageListView.setMessageFlagHandler { message: Message -> + // Handle when message is going to be flagged +} +messageListView.setMessagePinHandler { message: Message -> + // Handle when message is going to be pinned +} +messageListView.setMessageUnpinHandler { message: Message -> + // Handle when message is going to be unpinned +} +messageListView.setGiphySendHandler { giphyAction: GiphyAction -> + // Handle when some giphyAction is going to be performed +} +messageListView.setMessageRetryHandler { message: Message -> + // Handle when some failed message is going to be retried +} +messageListView.setMessageReactionHandler { message: Message, reactionType: String -> + // Handle when some reaction for message is going to be send +} +messageListView.setMessageReplyHandler { cid: String, message: Message -> + // Handle when message is going to be replied in the channel with cid +} +messageListView.setAttachmentDownloadHandler { + // Handle when attachment is going to be downloaded +} +``` + + + + +```java +messageListView.setLastMessageReadHandler(() -> { + // Handle when last message got read +}); +messageListView.setEndRegionReachedHandler(() -> { + // Handle when end region reached +}); +messageListView.setMessageDeleteHandler((message) -> { + // Handle when message is going to be deleted +}); +messageListView.setThreadStartHandler((message) -> { + // Handle when new thread for message is started +}); +messageListView.setMessageFlagHandler((message) -> { + // Handle when message is going to be flagged +}); +messageListView.setMessagePinHandler((message) -> { + // Handle when message is going to be pinned +}); +messageListView.setMessageUnpinHandler((message) -> { + // Handle when message is going to be unpinned +}); +messageListView.setGiphySendHandler((giphyAction) -> { + // Handle when some giphyAction is going to be performed +}); +messageListView.setMessageRetryHandler((message) -> { + // Handle when some failed message is going to be retried +}); +messageListView.setMessageReactionHandler((message, reactionType) -> { + // Handle when some reaction for message is going to be send +}); +messageListView.setMessageReplyHandler((cid, message) -> { + // Handle when message is going to be replied in the channel with cid +}); +messageListView.setAttachmentDownloadHandler((attachmentDownloadCall) -> { + // Handle when attachment is going to be downloaded +}); +messageListView.setMessageEditHandler((message) -> { + // Handle edit message +}); +``` + + + +:::note +Handlers must be set before passing any data to `MessageListView`. If you're not using the default binding provided by `bindView`, please make sure to set up all the handlers yourself. +::: + +This section lists only some of the more important handlers, many more exist and you can find there inside [`MessageListView`](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt). + +### Listeners + +In addition to the required handlers, `MessageListView` also provides optional listeners. They are also set by default if you use `bindView`. + +You can always override them to get the event when something happens: + + + + +```kotlin +messageListView.setMessageClickListener { message: Message -> + // Handle message being clicked +} +messageListView.setEnterThreadListener { message: Message -> + // Handle thread being entered +} +messageListView.setAttachmentDownloadClickListener { attachment: Attachment -> + // Handle clicks on the download attachment button +} +messageListView.setUserReactionClickListener { message: Message, user: User, reaction: Reaction -> + // Handle clicks on a reaction left by a user +} +messageListView.setMessageLongClickListener { message -> + // Handle message being long clicked +} +messageListView.setAttachmentClickListener { message, attachment -> + // Handle attachment being clicked +} +messageListView.setUserClickListener { user -> + // Handle user avatar being clicked +} +``` + + + + +```java +messageListView.setMessageClickListener((message) -> { + // Handle message being clicked +}); +messageListView.setEnterThreadListener((message) -> { + // Handle thread being entered +}); +messageListView.setAttachmentDownloadClickListener((attachment) -> { + // Handle clicks on the download attachment button +}); +messageListView.setUserReactionClickListener((message, user, reaction) -> { + // Handle clicks on a reaction left by a user +}); +messageListView.setMessageLongClickListener((message) -> { + // Handle message being long clicked +}); +messageListView.setAttachmentClickListener((message, attachment) -> { + // Handle attachment being clicked +}); +messageListView.setUserClickListener((user) -> { + // Handle user avatar being clicked +}); +``` + + + +Other available listeners for `MessageListView` can be found [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.messages.list.adapter/-message-list-listener-container/). + +## Previewing Attachments + +Out of the box previews are provided for the following attachment types: uploading, link, Giphy, image, video and file. + +### Image and Video + +Image and video attachments are previewed as thumbnails which can be displayed as a single tile or multiple ones depending on how many attachments are contained within the specific message. + +In practice they appear as such: + +| Video Thumbnails Enabled (Light Mode) | Video Thumbnails Enabled (Dark Mode) | +|---|---| +| ![Default Image and Video Attachment Previews Light Mode](../../assets/message_list_video_thumbs_enabled.png) | ![Default Image and Video Attachment Previews Dark Mode](../../assets/message_list_video_thumbs_enabled_dark.png) | + +Video thumbnails are a paid feature, with the pricing listed [here](https://getstream.io/chat/pricing/). They are enabled by default but can be turned off by setting the `ChatUI` property `videoThumbnailsEnabled` to `false`. You can read more about disabling video thumbnails in the [configuration documentation](../03-customizing-components.mdx#disabling-video-thumbnails) + +Once video thumbnails are disabled, messages containing video attachments will be displayed in the following manner: + +| Video Thumbnails Disabled (Light Mode) | Video Thumbnails Disabled (Dark Mode) | +|---|---| +| ![Video Thumbnails Disabled Light mode](../../assets/message_list_video_thumbs_disabled.png) | ![Video Thumbnails Disabled Light mode](../../assets/message_list_video_thumbs_disabled_dark.png) | + + + +## Customization + +You can change the appearance of this component to fit your app's design requirements. These changes can be done either at compile-time through the use of XML attributes, or at runtime by using style transformations. + +### Using XML Attributes + +`MessageListView` provides a large set of XML attributes available for customization. The complete list is available [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_view.xml). + +Let's consider an example in which we want to change the style of messages sent by the current user. + +| Light Mode | Dark Mode | +| --- | --- | +|![light](../../assets/message_style_xml_light.png)|![dark](../../assets/message_style_xml_dark.png)| + +In order to do that, we need to add additional attributes to `MessageListView`: + +```xml {11-14} + +``` + +### Using Style Transformations + +Both `MessageListView` and its ViewHolders can be configured programmatically (a list of supported customizations can be found [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.messages.list/-message-list-view-style/) and [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.messages.list/-message-list-item-style/)). + +As an example, let's apply the green style from the previous section, but this time programmatically: + +| Before | After | +| --- | --- | +|![message style before](../../assets/message_style_programmatically_message_before.png)|![message style after](../../assets/message_style_programmatically_message_after.png)| + +We are going to use a custom `TransformStyle.messageListItemStyleTransformer`: + + + + +```kotlin +TransformStyle.messageListItemStyleTransformer = StyleTransformer { defaultViewStyle -> + defaultViewStyle.copy( + messageBackgroundColorMine = Color.parseColor("#70AF74"), + messageBackgroundColorTheirs = Color.WHITE, + textStyleMine = defaultViewStyle.textStyleMine.copy(color = Color.WHITE), + textStyleTheirs = defaultViewStyle.textStyleTheirs.copy(color = Color.BLACK), + ) +} +``` + + + + +```java +TransformStyle.setMessageListItemStyleTransformer(source -> { + // Customize the theme + return source; +}); +``` + + + +:::note +The transformers should be set before the views are rendered to make sure that the new style was applied. +::: + +As another example, let's modify the default view which allows scrolling to the bottom when a new message arrives: + +| Before | After | +| --- | --- | +|![message style programmatically before](../../assets/message_style_programmatically_fab_before.png)|![message style programmatically after](../../assets/message_style_programmatically_fab_after.png)| + +To achieve this effect we need to provide this custom `TransformStyle.messageListStyleTransformer`: + + + + +```kotlin +TransformStyle.messageListStyleTransformer = StyleTransformer { defaultViewStyle -> + defaultViewStyle.copy( + scrollButtonViewStyle = defaultViewStyle.scrollButtonViewStyle.copy( + scrollButtonColor = Color.RED, + scrollButtonUnreadEnabled = false, + scrollButtonIcon = ContextCompat.getDrawable(requireContext(), R.drawable.stream_ui_ic_clock)!!, + ), + ) +} +``` + + + + +```java +TransformStyle.setMessageListStyleTransformer(source -> { + // Customize the theme + return source; +}); +``` + + + +## Channel Feature Flags + +Certain XML attributes let you to enable/disable features in `MessageListView`. +- `streamUiScrollButtonEnabled` - Show/hide the scroll-to-bottom button. +- `streamUiScrollButtonUnreadEnabled` - Show/hide the unread count badge on the scroll-to-bottom button. +- `streamUiReactionsEnabled` - Whether users can react to messages. +- `streamUiReplyEnabled` - Whether users can reply to messages. +- `streamUiCopyMessageActionEnabled` - Whether users can copy messages. +- `streamUiRetryMessageEnabled` - Whether users can retry failed messages. +- `streamUiEditMessageEnabled` - Whether users can edit their messages. +- `streamUiFlagMessageEnabled` - Whether users can flag messages. +- `streamUiFlagMessageConfirmationEnabled` - Whether users will see the confirmation dialog while flagging messages. +- `streamUiDeleteMessageEnabled` - Whether users can delete their messages. +- `streamUiDeleteConfirmationEnabled` - Whether users will see the confirmation dialog when deleting messages. +- `streamUiThreadsEnabled` - Whether users can create thread replies. +- `streamUiPinMessageEnabled` - Whether users can pin messages. + +These attributes let you enable/disable configuration for channel features. for example if a channel's configuration supports message replies, but you disabled it via XML attributes, then members of this channel won't see such an option. + +`MessageListView` provides you the possibility to enable/disable these channel features at runtime as well: + + + + +```kotlin +messageListView.setRepliesEnabled(false) +messageListView.setDeleteMessageEnabled(false) +messageListView.setEditMessageEnabled(false) +``` + + + + +```java +messageListView.setRepliesEnabled(false); +messageListView.setDeleteMessageEnabled(false); +messageListView.setEditMessageEnabled(false); +``` + + + +| Before | After | +| --- | --- | +|![message list options before](../../assets/message_list_options_before.png)|![message list options after](../../assets/message_list_options_after.png)| + +## Messages Start Position +You can configure the messages to start at the top or the bottom **(default)** of the view by using `streamUiMessagesStart` and `streamUiThreadMessagesStart` attributes. + +| Bottom | Top | +| --- | --- | +|![messages at the bottom](../../assets/messages_at_the_top.png)|![messages at the top](../../assets/messages_at_the_bottom.png)| + +:::note +The start position does not affect the orientation. The default is from bottom to top. If you would like to change that, use the method `setCustomLinearLayoutManager` and set a `LinearLayoutManager` with the desired orientation. +::: + + +## Filtering Messages + +You can filter out certain messages if you don't want to show them in the `MessageListView`. +Imagine you want to hide all messages which contain the word 'secret'. This can be achieved using the following code: + + + + +```kotlin +val forbiddenWord = "secret" +val predicate = MessageListView.MessageListItemPredicate { item -> + !(item is MessageListItem.MessageItem && item.message.text.contains(forbiddenWord)) +} +messageListView.setMessageListItemPredicate(predicate) +``` + + + + +```java +String forbiddenWord = "secret"; +messageListView.setMessageListItemPredicate(item -> { + if (item instanceof MessageListItem.MessageItem) { + MessageListItem.MessageItem messageItem = (MessageListItem.MessageItem) item; + return !((MessageListItem.MessageItem) item).getMessage().getText().contains(forbiddenWord); + } + + return true; +}); +``` + + + +:::note +The predicate has to return `true` for the items that you _do_ want to display in the list. +::: + +## Custom Message Views + +`MessageListView` provides an API for creating custom ViewHolders. To use your own ViewHolder: +1. Extend `MessageListItemViewHolderFactory`. +2. Write your own logic for creating ViewHolders. +3. Create a new factory instance and set it on `MessageListView`. + +Let's consider an example in which we want to create a custom ViewHolder for messages sent by other users less than 24 hours ago. The result should look like this: + +| ![](../../assets/message_list_custom_vh_factory.png) | +| --- | + +1. Add a new layout called `today_message_list_item.xml`: + +``` xml + + + + + + + + + + + + +``` + +2. Add a new `TodayViewHolder` class that inflates this layout and populates it with data: + + + + +```kotlin +class TodayViewHolder( + parentView: ViewGroup, + private val binding: TodayMessageListItemBinding = TodayMessageListItemBinding.inflate(LayoutInflater.from( + parentView.context), + parentView, + false), +) : BaseMessageItemViewHolder(binding.root) { + + override fun bindData(data: MessageListItem.MessageItem, diff: MessageListItemPayloadDiff?) { + binding.textLabel.text = data.message.text + } +} +``` + + + + +```java +class TodayViewHolder extends BaseMessageItemViewHolder { + + TodayMessageListItemBinding binding; + + public TodayViewHolder(@NonNull ViewGroup parentView, @NonNull TodayMessageListItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + @Override + public void bindData(@NonNull MessageListItem.MessageItem data, @Nullable MessageListItemPayloadDiff diff) { + binding.textLabel.setText(data.getMessage().getText()); + } +} +``` + + + +3. Add a new `CustomMessageViewHolderFactory` class that evaluates each message, and uses the custom ViewHolder when necessary: + + + + +```kotlin +class CustomMessageViewHolderFactory : MessageListItemViewHolderFactory() { + override fun getItemViewType(item: MessageListItem): Int { + return if (item is MessageListItem.MessageItem && + item.isTheirs && + item.message.attachments.isEmpty() && + item.message.createdAt.isLessThenDayAgo() + ) { + TODAY_VIEW_HOLDER_TYPE + } else { + super.getItemViewType(item) + } + } + + private fun Date?.isLessThenDayAgo(): Boolean { + if (this == null) { + return false + } + val dayInMillis = TimeUnit.DAYS.toMillis(1) + return time >= System.currentTimeMillis() - dayInMillis + } + + override fun createViewHolder( + parentView: ViewGroup, + viewType: Int, + ): BaseMessageItemViewHolder { + return if (viewType == TODAY_VIEW_HOLDER_TYPE) { + TodayViewHolder(parentView) + } else { + super.createViewHolder(parentView, viewType) + } + } + + companion object { + private const val TODAY_VIEW_HOLDER_TYPE = 1 + } +} +``` + + + + +```java +class CustomMessageViewHolderFactory extends MessageListItemViewHolderFactory { + + private int TODAY_VIEW_HOLDER_TYPE = 1; + + @Override + public int getItemViewType(@NonNull MessageListItem item) { + if (item instanceof MessageListItem.MessageItem) { + MessageListItem.MessageItem messageItem = (MessageListItem.MessageItem) item; + if (messageItem.isTheirs() + && messageItem.getMessage().getAttachments().isEmpty() + && isLessThanDayAgo((messageItem.getMessage().getCreatedAt()))) { + return TODAY_VIEW_HOLDER_TYPE; + } + } + + return super.getItemViewType(item); + + } + + private boolean isLessThanDayAgo(Date date) { + if (date == null) return false; + long dayInMillis = TimeUnit.DAYS.toMillis(1); + + return date.getTime() >= System.currentTimeMillis() - dayInMillis; + } + + @NonNull + @Override + public BaseMessageItemViewHolder createViewHolder(@NonNull ViewGroup parentView, int viewType) { + if (viewType == TODAY_VIEW_HOLDER_TYPE) { + return new TodayViewHolder(parentView, TodayMessageListItemBinding.inflate(LayoutInflater.from(parentView.getContext()), parentView, false)); + } + return super.createViewHolder(parentView, viewType); + } +} +``` + + + +4. Finally, set an instance of the custom factory on `MessageListView`: + + + + +```kotlin +messageListView.setMessageViewHolderFactory(CustomMessageViewHolderFactory()) +``` + + + + +```java +messageListView.setMessageViewHolderFactory(new CustomMessageViewHolderFactory()); +``` + + + +## Custom Empty State + +`MessageListView` handles loading and empty states out of the box. If you want to customize these, you can do it at runtime. + +Let's consider an example where you want to set a custom empty state: + + + + +```kotlin +val textView = TextView(context).apply { + text = "There are no messages yet" + setTextColor(Color.RED) +} +messageListView.setEmptyStateView( + view = textView, + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) +) +``` + + + + +```java +TextView textView = new TextView(getContext()); +textView.setText("There are no messages yet"); +textView.setTextColor(Color.RED); +messageListView.setEmptyStateView( + textView, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) +); +``` + + + +This code will display the following empty state: + +| ![The custom empty state in action](../../assets/message_lis_custom_empty_state.png) | +| --- | +## Configure When the User Avatar Appears +By default, user avatars will appear next to messages that are either the last message in a group of messages, or the only one within it. + +| ![](../../assets/message_list_show_avatar_predicate_default.png) | +|---| + +You can configure when the user avatar will appear by setting your own predicate using `MessageListView.setShowAvatarPredicate()`.
+For instance, the following code will make the user avatar appear next to all messages sent by other users, regardless of their position. + + + + +```kotlin +messageListView.setShowAvatarPredicate { messageItem -> + messageItem.isTheirs +} +``` + + + + +```java +messageListView.setShowAvatarPredicate((messageItem) -> + messageItem.isTheirs() +); +``` + + + +The result looks like this: + +| ![](../../assets/message_list_show_avatar_predicate_custom.png) | +|---| + +:::note +To avoid overlap between the avatar and the message bubble, remember to use `streamUiMessageStartMargin` and `streamUiMessageEndMargin` to create space for the message avatar. +::: + +If you set a predicate that shows avatars for your own messages as well, use the following value: + +```xml +streamUiMessageEndMargin="@dimen/stream_ui_message_viewholder_avatar_missing_margin" +``` + +If your predicate doesn't show avatars for your own messages (the default behavior), remove the end margin: + +```xml +streamUiMessageEndMargin="0dp" +``` +### Giphy Sizing Modes + +We offer two sizing modes: + +* `adaptive`: The container will automatically resize itself to respect the aspect ratio of the Giphy it is hosting. +* `fixed_size`: The container will retain a fixed size, regardless of the aspect ratio of the Giphy it is hosting. + +If you use adaptive sizing, you're all set. However, if you use fixed sizing, you also need to set the container dimensions. + +You have the following attributes at your disposal to do so: + +* `streamUiGiphyMediaAttachmentWidth`: Sets the width of the Giphy container. +* `streamUiGiphyMediaAttachmentHeight`: Sets the height of the Giphy container. +* `streamUiGiphyMediaAttachmentDimensionRatio`: Sets the dimension ratio of the Giphy container. + +To apply them, include them in your app's theme. You can find out more about theming in the [Theming documentation page](../02-theming.mdx). + +### Giphy Types + +The following represent the available types(quality modes): + +* `original`: The original Giphy quality. +* `fixedHeight`: Usually results in a slightly lower quality than the original, but improves performance. +* `fixedHeightDownsampled`: Lower visual fidelity than the original along with a lower FPS (frames per second) count. + +### Scale type: + +The scale type affects how the Giphys are rendered inside the container with regards to the difference in their sizes and aspect ratios. If you are using adaptive sizing, then it's best not to change this setting. If however you decide to use fixed height sizing, then you can fine tune the way Giphys are displayed. + +We support every scale type used by the stock Android `ImageView` attribute `scaleType`, some of them being: + +* `fitCenter`: Changes the aspect ratio of the Giphy in order to fit it inside its container. +* `centerCrop`: Centers the Giphy and crops it to the aspect ratio of its container. +* `center`: Doesn't scale the image and centers it inside its container. +* ... diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/04-search-view.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/04-search-view.mdx new file mode 100644 index 00000000000..d9814027e1b --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/04-search-view.mdx @@ -0,0 +1,190 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Searching for Messages + +The `SearchInputView` and `SearchResultListView` components can be used to search and display messages that contain specific text. The search is performed across all channels a user is a member of. + +| Light Mode | Dark Mode | +| --- | --- | +|![search view light](../../assets/search_view_hey_light.png)|![search view dark](../../assets/search_view_hey_dark.png)| + +## Usage + +Here's an example layout using these two Views: + +```xml + + + + + + + + +``` + +We recommend using `SearchViewModel` to get search results from the Stream API and then render them using the `SearchResultListView`. + +The basic setup of the ViewModel and connecting it to the View is done the following way: + + + + +```kotlin +// Instantiate the ViewModel +val searchViewModel: SearchViewModel by viewModels() + +// Bind the ViewModel with SearchResultListView +searchViewModel.bindView(searchResultListView, viewLifecycleOwner) +``` + + + + +```java +// Instantiate the ViewModel +SearchViewModel viewModel = new ViewModelProvider(this).get(SearchViewModel.class); + +// Bind it with SearchResultListView +SearchViewModelBinding.bind(viewModel, searchResultListView, getViewLifecycleOwner()); +``` + + + +Finally, start the search by passing the search query to the ViewModel: + + + + +```kotlin +// Notify ViewModel when search is triggered +searchInputView.setSearchStartedListener(viewModel::setQuery) +``` + + + + +```java +// Notify ViewModel when search is triggered +searchInputView.setSearchStartedListener(viewModel::setQuery); +``` + + + +:::note +`bindView` sets listeners on the view and the ViewModel. Any additional listeners should be set _after_ calling `bindView`. +::: + +## Handling Actions + +In addition to the `SearchStartedListener` described above, `SearchInputView` allows you to listen for text changes by using listeners: + + + + +```kotlin +searchInputView.setContinuousInputChangedListener { query -> + // Search query changed +} +searchInputView.setDebouncedInputChangedListener { query -> + // Search query changed and has been stable for a short while +} +``` + + + + +```java +searchInputView.setContinuousInputChangedListener(query -> { + // Search query changed +}); +searchInputView.setDebouncedInputChangedListener(query -> { + // Search query changed and has been stable for a short while +}); +``` + + + +`SearchResultListView` exposes a listener for handling item clicks: + + + + +```kotlin +searchResultListView.setSearchResultSelectedListener { message -> + // Handle search result click +} +``` + + + + +```java +searchResultListView.setSearchResultSelectedListener(message -> { + // Handle search result click +}); +``` + + + +The full list of listeners available for `SearchInputView` can be found [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.search/-search-input-view/), and for `SearchResultListView` [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.search.list/-search-result-list-view/). + +## Updating the Search Query Programmatically + +`SearchInputView` provides a way to change the search query programmatically: + + + + +```kotlin +searchInputView.setQuery("query") +``` + + + + +```java +searchInputView.setQuery("query"); +``` + + + +You can also easily clear the current input: + + + + +```kotlin +searchInputView.clear() +``` + + + + +```java +searchInputView.clear(); +``` + + + +:::note +Updating the search query programmatically automatically notifies corresponding listeners. +::: diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/05-mentions-and-pinned-messages.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/05-mentions-and-pinned-messages.mdx new file mode 100644 index 00000000000..d30026cb601 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/05-mentions-and-pinned-messages.mdx @@ -0,0 +1,156 @@ +# Mentions and Pinned Messages + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Mention List + +`MentionListView` is a UI Component that shows previews of messages that contain mentions of the current user. + +| Light Mode | Dark Mode | +| --- | --- | +|![Light mode](../../assets/mentions_list_view_light.png)|![Dark mode](../../assets/mentions_list_view_dark.png)| + +### Usage + +You can add this View via XML: + +```xml + +``` + +We recommend using this view with its [ViewModel](../01-getting-started.mdx#viewmodels), which supplies it with data from the Stream API. + +The basic setup of the ViewModel and connecting it to the View is done the following way: + + + + +```kotlin +val viewModel: MentionListViewModel by viewModels() +viewModel.bindView(mentionListView, viewLifecycleOwner) +``` + + + + +```java +MentionListViewModel viewModel = new ViewModelProvider(this).get(MentionListViewModel.class); +MentionListViewModelBinding.bind(viewModel, mentionListView, getViewLifecycleOwner()); +``` + + + +From that point, you should be able to see messages which contain mentions of the current user. + +:::note +`bindView` sets listeners on the View and the ViewModel. Any additional listeners should be set _after_ calling `bindView`. +::: + +### Handling Actions + +`MentionListView` allows you to configure certain actions on it: + + + + +```kotlin +mentionListView.setMentionSelectedListener { message -> + // Handle a mention item being clicked +} +``` + + + + +```java +mentionListView.setMentionSelectedListener(message -> { + // Handle a mention item being clicked +}); +``` + + + +The full list of available listeners is available [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.mentions.list/-mention-list-view/). + +## Pinned Message List + +`PinnedMessageListView` is a UI Component that shows a list of pinned messages. + +| Light Mode | Dark Mode | +| --- | --- | +|![Light mode](../../assets/pinned_message_list_view_light.png)|![Dark mode](../../assets/pinned_message_list_view_dark.png)| + +### Usage + +You can add this View via XML: + +```xml + +``` + +We recommend using this view with its [ViewModel](../01-getting-started.mdx#viewmodels), which supplies it with data from the Stream API. + +The basic setup of the ViewModel and connecting it to the View is done the following way: + + + + +```kotlin +val viewModel: PinnedMessageListViewModel by viewModels { + PinnedMessageListViewModelFactory(cid = "messaging:123") +} +viewModel.bindView(pinnedMessageListView, viewLifecycleOwner) +``` + + + + +```java +ViewModelProvider.Factory factory = new PinnedMessageListViewModelFactory.Builder() + .cid("messaging:123") + .build(); +PinnedMessageListViewModel viewModel = new ViewModelProvider(this, factory).get(PinnedMessageListViewModel.class); + +PinnedMessageListViewModelBinding.bind(viewModel, pinnedMessageListView, getViewLifecycleOwner()); +``` + + + +From that point, you should be able to see the list of pinned messages. + +:::note +`bindView` sets listeners on the View and the ViewModel. Any additional listeners should be set _after_ calling `bindView`. +::: + +### Handling Actions + +`PinnedMessageListView` allows you to configure certain actions on it: + + + + +```kotlin +pinnedMessageListView.setPinnedMessageSelectedListener { message -> + // Handle a pinned message item being clicked +} +``` + + + + +```java +pinnedMessageListView.setPinnedMessageSelectedListener(message -> { + // Handle a mention item being clicked +}); +``` + + + +The full list of available listeners is available [here](https://getstream.github.io/stream-chat-android/stream-chat-android-ui-components/io.getstream.chat.android.ui.feature.pinned.list/-pinned-message-list-view/). diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/_category_.json b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/_category_.json new file mode 100644 index 00000000000..345a3c4498c --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/05-message-list/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Message List" +} diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/01-message-composer.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/01-message-composer.mdx new file mode 100644 index 00000000000..35251ab16cc --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/01-message-composer.mdx @@ -0,0 +1,851 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Message Composer + +`MessageComposerView` is a UI component for sending messages and attachments to channels. + +| Light Mode | Dark Mode | + |--------------------------------------------------|------------------------------------------------------| +| ![Light_mode](../../assets/message_composer.png) | ![Dark_mode](../../assets/message_composer_dark.png) | + +It supports the following features: + +* Attachments +* Slash Commands +* Typing events +* Editing messages +* Threads +* Mentions +* Replies + +Let's see how to integrate the new `MessageComposerView` in your UI. + +## Usage + +To use `MessageComposerView`, include it in your XML layout. + +```xml + +``` + +The recommended way of setting up `MessageComposerView` is by binding it to the `MessageComposerViewModel`. This will make it fully functional by setting up any necessary listeners and data handling. + + + + +```kotlin +// Create MessageComposerViewModel for a given channel +val factory = MessageListViewModelFactory(cid = "messaging:123") +val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } + +// Bind MessageComposerViewModel with MessageComposerView +messageComposerViewModel.bindView( + // Required + messageComposerView, + viewLifecycleOwner, + // Optional (you can set your custom listeners here) + sendMessageButtonClickListener = { + // Handle send button click + }, + textInputChangeListener = { text -> + // Handle input text change + }, + //... + // other listeners +) +``` + + + + +```java +// Create MessageComposerViewModel for a given channel +ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder() + .cid("messaging:123") + .build(); +ViewModelProvider provider = new ViewModelProvider(this, factory); +MessageComposerViewModel viewModel = provider.get(MessageComposerViewModel.class); + +// Bind MessageComposerViewModel with MessageComposerView +MessageComposerViewModelBinder.with(viewModel) + // Optional (you can set your custom listeners here) + .onSendMessageButtonClick((message) -> { + // Handle send button click + return Unit.INSTANCE; + }) + .onTextInputChange((text) -> { + // Handle input text change + return Unit.INSTANCE; + }) + //... + // other listeners + + // Required + .bind(messageComposerView, getViewLifecycleOwner()); +``` + + + +Because it doesn't make sense to use the `MessageComposerView` as a standalone component, you also need to integrate it with the `MessageListView`: + +```xml + + + + + + + +``` + + + + +```kotlin +// Create ViewModels for MessageComposerView and MessageListView +val factory = MessageListViewModelFactory(cid = "messaging:123") +val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } +val messageListViewModel: MessageListViewModel by viewModels { factory } + +// Bind MessageComposerViewModel with MessageComposerView +messageComposerViewModel.bindView(messageComposerView, viewLifecycleOwner) + +// Bind MessageListViewModel with MessageListView +messageListViewModel.bindView(messageListView, viewLifecycleOwner) + +// Integrate MessageComposerView with MessageListView +messageListViewModel.mode.observe(viewLifecycleOwner) { mode -> + when (mode) { + is MessageMode.MessageThread -> { + messageComposerViewModel.setMessageMode(MessageMode.MessageThread(mode.parentMessage)) + } + is MessageMode.Normal -> { + messageComposerViewModel.leaveThread() + } + } +} +messageListView.setMessageReplyHandler { _, message -> + messageComposerViewModel.performMessageAction(Reply(message)) +} +messageListView.setMessageEditHandler { message -> + messageComposerViewModel.performMessageAction(Edit(message)) +} +``` + + + + +```java +// Create ViewModels for MessageComposerView and MessageListView +ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder() + .cid("messaging:123") + .build(); +ViewModelProvider provider = new ViewModelProvider(this, factory); +MessageComposerViewModel messageComposerViewModel = provider.get(MessageComposerViewModel.class); +MessageListViewModel messageListViewModel = provider.get(MessageListViewModel.class); + +// Bind MessageComposerViewModel with MessageComposerView +MessageComposerViewModelBinder.with(messageComposerViewModel).bind(messageComposerView, getViewLifecycleOwner()); + +// Bind MessageListViewModel with MessageListView +MessageListViewModelBinding.bind(messageListViewModel, messageListView, getViewLifecycleOwner()); + +// Integrate MessageComposerView with MessageListView +messageListViewModel.getMode().observe(getViewLifecycleOwner(), mode -> { + if (mode instanceof MessageMode.MessageThread) { + messageComposerViewModel.setMessageMode(new MessageMode.MessageThread(((MessageMode.MessageThread) mode).getParentMessage())); + } else if (mode instanceof MessageMode.Normal) { + messageComposerViewModel.leaveThread(); + } +}); +messageListView.setMessageReplyHandler((cid, message) -> messageComposerViewModel.performMessageAction(new Reply(message))); +messageListView.setMessageEditHandler((message) -> messageComposerViewModel.performMessageAction(new Edit(message))); +``` + + + +In the snippet above, you initialize the message composer and integrate it with the `MessageListView` by passing actions from the message list to the composer. + +This will produce a fully working solution, as shown in the image below. + +| ![Whole Screen](../../assets/message_composer_whole_screen.png) | +|-----------------------------------------------------------------| + +## Handling Actions + +To handle actions supported by the `MessageComposerView` you can set the corresponding listeners: + + + + +```kotlin +messageComposerView.sendMessageButtonClickListener = { + // Handle send button click +} +messageComposerView.textInputChangeListener = { text -> + // Handle input text change +} +messageComposerView.attachmentSelectionListener = { attachments -> + // Handle attachment selection +} +messageComposerView.attachmentRemovalListener = { attachment -> + // Handle attachment removal +} +messageComposerView.mentionSelectionListener = { user -> + // Handle mention selection +} +messageComposerView.commandSelectionListener = { command -> + // Handle command selection +} +messageComposerView.alsoSendToChannelSelectionListener = { checked -> + // Handle "also send to channel" checkbox selection +} +messageComposerView.dismissActionClickListener = { + // Handle dismiss action button click +} +messageComposerView.commandsButtonClickListener = { + // Handle commands button click +} +messageComposerView.dismissSuggestionsListener = { + // Handle when suggestions popup is dismissed +} +messageComposerView.audioRecordButtonLockListener = { + // Handle audio record button lock +} + +messageComposerView.audioRecordButtonHoldListener = { + // Handle audio record button hold +} + +messageComposerView.audioRecordButtonCancelListener = { + // Handle audio record button cancel +} + +messageComposerView.audioRecordButtonReleaseListener = { + // Handle audio record button release +} + +messageComposerView.audioDeleteButtonClickListener = { + // Handle audio delete button click +} + +messageComposerView.audioStopButtonClickListener = { + // Handle audio stop button click +} + +messageComposerView.audioPlaybackButtonClickListener = { + // Handle audio playback button click +} + +messageComposerView.audioCompleteButtonClickListener = { + // Handle audio complete button click +} + +messageComposerView.audioSliderDragStartListener = { progress -> + // Handle audio slider drag start +} + +messageComposerView.audioSliderDragStopListener = { progress -> + // Handle audio slider drag stop +} +messageComposerView.attachmentsButtonClickListener = { + // Handle attachments button click +} +``` + + + + +```java +messageComposerView.setSendMessageButtonClickListener(() -> { + // Handle send button click + return Unit.INSTANCE; +}); +messageComposerView.setTextInputChangeListener((text) -> { + // Handle input text change + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentSelectionListener((attachments) -> { + // Handle attachment selection + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentRemovalListener((attachment) -> { + // Handle attachment removal + return Unit.INSTANCE; +}); +messageComposerView.setMentionSelectionListener((user) -> { + // Handle mention selection + return Unit.INSTANCE; +}); +messageComposerView.setCommandSelectionListener((command) -> { + // Handle command selection + return Unit.INSTANCE; +}); +messageComposerView.setAlsoSendToChannelSelectionListener((checked) -> { + // Handle "also send to channel" checkbox selection + return Unit.INSTANCE; +}); +messageComposerView.setDismissActionClickListener(() -> { + // Handle dismiss action button click + return Unit.INSTANCE; +}); +messageComposerView.setCommandsButtonClickListener(() -> { + // Handle commands button click + return Unit.INSTANCE; +}); +messageComposerView.setDismissSuggestionsListener(() -> { + // Handle when suggestions popup is dismissed + return Unit.INSTANCE; +}); +messageComposerView.setDismissSuggestionsListener(() -> { + // Handle when suggestions popup is dismissed + return Unit.INSTANCE; +}); +messageComposerView.setAudioRecordButtonLockListener(() -> { + // Handle audio record button lock + return Unit.INSTANCE; +}); +messageComposerView.setAudioRecordButtonHoldListener(() -> { + // Handle audio record button hold + return Unit.INSTANCE; +}); +messageComposerView.setAudioRecordButtonCancelListener(() -> { + // Handle audio record button cancel + return Unit.INSTANCE; +}); +messageComposerView.setAudioRecordButtonReleaseListener(() -> { + // Handle audio record button release + return Unit.INSTANCE; +}); +messageComposerView.setAudioDeleteButtonClickListener(() -> { + // Handle audio delete button click + return Unit.INSTANCE; +}); +messageComposerView.setAudioStopButtonClickListener(() -> { + // Handle audio stop button click + return Unit.INSTANCE; +}); +messageComposerView.setAudioPlaybackButtonClickListener(() -> { + // Handle audio playback button click + return Unit.INSTANCE; +}); +messageComposerView.setAudioCompleteButtonClickListener(() -> { + // Handle audio complete button click + return Unit.INSTANCE; +}); +messageComposerView.setAudioSliderDragStartListener((progress) -> { + // Handle audio slider drag start + return Unit.INSTANCE; +}); +messageComposerView.setAudioSliderDragStopListener((progress) -> { + // Handle audio slider drag stop + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentsButtonClickListener(() -> { + // Handle attachments button click + return Unit.INSTANCE; +}); +``` + + + +If you don't set your custom listeners, the default listeners from the `MessageComposerViewModel::bindView` method will be used: + + + + +```kotlin +messageComposerView.sendMessageButtonClickListener = { + messageComposerViewModel.sendMessage() +} +messageComposerView.textInputChangeListener = { text -> + messageComposerViewModel.setMessageInput(text) +} +messageComposerView.attachmentSelectionListener = { attachments -> + messageComposerViewModel.addSelectedAttachments(attachments) +} +messageComposerView.attachmentRemovalListener = { attachment -> + messageComposerViewModel.removeSelectedAttachment(attachment) +} +messageComposerView.mentionSelectionListener = { user -> + messageComposerViewModel.selectMention(user) +} +messageComposerView.commandSelectionListener = { command -> + messageComposerViewModel.selectCommand(command) +} +messageComposerView.alsoSendToChannelSelectionListener = { checked -> + messageComposerViewModel.setAlsoSendToChannel(checked) +} +messageComposerView.dismissActionClickListener = { + messageComposerViewModel.dismissMessageActions() +} +messageComposerView.commandsButtonClickListener = { + messageComposerViewModel.toggleCommandsVisibility() +} +messageComposerView.dismissSuggestionsListener = { + messageComposerViewModel.dismissSuggestionsPopup() +} +messageComposerView.attachmentsButtonClickListener = { + // Handle attachments button click +} +``` + + + + +```java +messageComposerView.setSendMessageButtonClickListener(() -> { + messageComposerViewModel.sendMessage(); + return Unit.INSTANCE; +}); +messageComposerView.setTextInputChangeListener((text) -> { + messageComposerViewModel.setMessageInput(text); + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentSelectionListener((attachments) -> { + messageComposerViewModel.addSelectedAttachments(attachments); + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentRemovalListener((attachment) -> { + messageComposerViewModel.removeSelectedAttachment(attachment); + return Unit.INSTANCE; +}); +messageComposerView.setMentionSelectionListener((user) -> { + messageComposerViewModel.selectMention(user); + return Unit.INSTANCE; +}); +messageComposerView.setCommandSelectionListener((command) -> { + messageComposerViewModel.selectCommand(command); + return Unit.INSTANCE; +}); +messageComposerView.setAlsoSendToChannelSelectionListener((checked) -> { + messageComposerViewModel.setAlsoSendToChannel(checked); + return Unit.INSTANCE; +}); +messageComposerView.setDismissActionClickListener(() -> { + messageComposerViewModel.dismissMessageActions(); + return Unit.INSTANCE; +}); +messageComposerView.setCommandsButtonClickListener(() -> { + messageComposerViewModel.toggleCommandsVisibility(); + return Unit.INSTANCE; +}); +messageComposerView.setDismissSuggestionsListener(() -> { + messageComposerViewModel.dismissSuggestionsPopup(); + return Unit.INSTANCE; +}); +messageComposerView.setAttachmentsButtonClickListener(() -> { + // Handle attachments button click + return Unit.INSTANCE; +}); +``` + + + +Now let's see how to customize the view. + +## Customization + +`MessageComposerView` can be customized: + +- Using XML Attributes +- Using style Transformations +- By overriding content Views + +### Using XML Attributes + +The styling of the View can be configured by styled attributes. You can change the color of the message input, the fonts, visibility of various components and so on. The full list of available attributes can be found [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_message_composer_view.xml). + +Here's an example of setting a custom attribute: + +```xml + +``` + +This produces the following styling: + +| ![Custom Attribute](../../assets/message_composer_custom_attribute.png) | +|-------------------------------------------------------------------------| + +Different configurations can be used to achieve the desired appearance of `MessageComposerView`. If you don't need to change the View's appearance at runtime, using styled attributes should be enough. However, if you want to customize it at runtime, then you can use `MessageComposerViewStyle` as described in the next section. + +### Using Style Transformations + +You can use [TransformStyle](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/TransformStyle.kt) to apply global style transformations to all `MessageComposerView` instances. For example, you can create a `messageComposerStyleTransformer` like this one to change the input text color: + + + + +```kotlin +TransformStyle.messageComposerStyleTransformer = StyleTransformer { viewStyle -> + viewStyle.copy( + messageInputTextStyle = viewStyle.messageInputTextStyle.copy( + color = ContextCompat.getColor(context, R.color.stream_ui_accent_red) + ) + ) +} +``` + + + + +```java +TransformStyle.setMessageComposerStyleTransformer(source -> { + // Customize the style + return source; +}); +``` + + + +:::note +The transformer should be set before the View is rendered to make sure that the new style was applied. +::: + +## Overriding Content Views + +With the new `MessageComposerView` you can replace certain parts of the layout with custom content views. There are several parts available for customization. + +* **Leading content**: Represents the left part with integration buttons. +* **Center content**: Represents the center part with the text input. +* **Trailing content**: Represents the right part with the send button. +* **Header content**: Represents the top part with the action mode title. +* **Footer content**: Represents the bottom part with the "also send to channel" checkbox. +* **Command suggestions content**: Represents the content inside the command suggestions popup. +* **Mention suggestions content**: Represents the content inside the mention suggestions popup. + +The available methods with the default content view implementations are listed below: + + + + +```kotlin +messageComposerView.setLeadingContent( + DefaultMessageComposerLeadingContent(context).also { + it.attachmentsButtonClickListener = { messageComposerView.attachmentsButtonClickListener() } + it.commandsButtonClickListener = { messageComposerView.commandsButtonClickListener() } + } +) +messageComposerView.setCenterContent( + DefaultMessageComposerCenterContent(context).also { + it.textInputChangeListener = { text -> messageComposerView.textInputChangeListener(text) } + it.attachmentRemovalListener = { attachment -> messageComposerView.attachmentRemovalListener(attachment) } + } +) +messageComposerView.setTrailingContent( + DefaultMessageComposerTrailingContent(context).also { + it.sendMessageButtonClickListener = { messageComposerView.sendMessageButtonClickListener() } + } +) +messageComposerView.setHeaderContent( + DefaultMessageComposerHeaderContent(context).also { + it.dismissActionClickListener = { messageComposerView.dismissActionClickListener() } + } +) +messageComposerView.setFooterContent( + DefaultMessageComposerFooterContent(context).also { + it.alsoSendToChannelSelectionListener = { checked -> messageComposerView.alsoSendToChannelSelectionListener(checked) } + } +) +messageComposerView.setCommandSuggestionsContent( + DefaultMessageComposerCommandSuggestionsContent(context).also { + it.commandSelectionListener = { command -> messageComposerView.commandSelectionListener(command) } + } +) +messageComposerView.setMentionSuggestionsContent( + DefaultMessageComposerMentionSuggestionsContent(context).also { + it.mentionSelectionListener = { user -> messageComposerView.mentionSelectionListener(user) } + } +) +``` + + + + +```java +DefaultMessageComposerLeadingContent leadingContent = new DefaultMessageComposerLeadingContent(context); +leadingContent.setAttachmentsButtonClickListener(() -> messageComposerView.getAttachmentsButtonClickListener().invoke()); +leadingContent.setCommandsButtonClickListener(() -> messageComposerView.getCommandsButtonClickListener().invoke()); + +messageComposerView.setLeadingContent(leadingContent); + +DefaultMessageComposerCenterContent centerContent = new DefaultMessageComposerCenterContent(context); +centerContent.setTextInputChangeListener((text) -> messageComposerView.getTextInputChangeListener().invoke(text)); +centerContent.setAttachmentRemovalListener((attachment -> messageComposerView.getAttachmentRemovalListener().invoke(attachment))); + +messageComposerView.setCenterContent(centerContent); + +DefaultMessageComposerTrailingContent trailingContent = new DefaultMessageComposerTrailingContent(context); +trailingContent.setSendMessageButtonClickListener(() -> messageComposerView.getSendMessageButtonClickListener().invoke()); + +messageComposerView.setTrailingContent(trailingContent); + +DefaultMessageComposerHeaderContent headerContent = new DefaultMessageComposerHeaderContent(context); +headerContent.setDismissActionClickListener(() -> messageComposerView.getDismissActionClickListener().invoke()); +messageComposerView.setHeaderContent(headerContent); + +DefaultMessageComposerFooterContent footerContent = new DefaultMessageComposerFooterContent(context); +footerContent.setAlsoSendToChannelSelectionListener((checked) -> messageComposerView.getAlsoSendToChannelSelectionListener().invoke(checked)); +messageComposerView.setFooterContent(footerContent); + +DefaultMessageComposerCommandSuggestionsContent commandSuggestionsContent = new DefaultMessageComposerCommandSuggestionsContent(context); +commandSuggestionsContent.setCommandSelectionListener((command) -> messageComposerView.getCommandSelectionListener().invoke(command)); +messageComposerView.setCommandSuggestionsContent(commandSuggestionsContent); + +DefaultMessageComposerMentionSuggestionsContent mentionSuggestionsContent = new DefaultMessageComposerMentionSuggestionsContent(context); +mentionSuggestionsContent.setMentionSelectionListener((user) -> messageComposerView.getMentionSelectionListener().invoke(user)); +messageComposerView.setMentionSuggestionsContent(mentionSuggestionsContent); +``` + + + +To create a custom content view you need to create an Android View that implements the `MessageComposerContent` interface: + + + + +```kotlin +class CustomMessageComposerLeadingContent : FrameLayout, MessageComposerContent { + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + override fun attachContext(messageComposerContext: MessageComposerContext) { + // Access the style if necessary + val style = messageComposerContext.style + } + + override fun renderState(state: MessageComposerState) { + // Render the state of the component + } +} +``` + + + + +```java +public class CustomMessageComposerLeadingContent extends FrameLayout implements MessageComposerContent { + + public CustomMessageComposerLeadingContent(@NonNull Context context) { + this(context, null); + } + + public CustomMessageComposerLeadingContent(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomMessageComposerLeadingContent(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void attachContext(@NonNull MessageComposerContext messageComposerContext) { + // Access the style if necessary + MessageComposerViewStyle style = messageComposerContext.getStyle(); + } + + @Override + public void renderState(@NonNull MessageComposerState state) { + // Render the state of the component + } +} +``` + + + +Notice that you need to implement 2 methods from the `MessageComposerContent` interface: + +- `attachContext()` Called only once when the View has been attached to the hierarchy. +- `renderState()` Invoked when the state has changed and the UI needs to be updated accordingly. + +Finally, you need to pass the created content view to the `MessageComposerView`: + + + + +```kotlin +messageComposerView.setLeadingContent(CustomMessageComposerLeadingContent(context)) +``` + + + + +```java +CustomMessageComposerLeadingContent leadingContent = new CustomMessageComposerLeadingContent(context); +messageComposerView.setLeadingContent(leadingContent); +``` + + + +Here is an example of how the leading content can be customized to show a date picker button: + +```xml + + +``` + + + + +```kotlin +class CustomMessageComposerLeadingContent : FrameLayout, MessageComposerContent { + + private lateinit var binding: CustomMessageComposerLeadingContentBinding + + var datePickerButtonClickListener: () -> Unit = {} + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + binding = CustomMessageComposerLeadingContentBinding.inflate(LayoutInflater.from(context), this, true) + binding.datePickerButton.setOnClickListener { datePickerButtonClickListener() } + } + + override fun attachContext(messageComposerContext: MessageComposerContext) { + // Access the style if necessary + val style = messageComposerContext.style + } + + override fun renderState(state: MessageComposerState) { + // Render the state of the component + } +} +``` + + + + +```java +public class CustomMessageComposerLeadingContent extends FrameLayout implements MessageComposerContent { + + private MessageComposerLeadingContentBinding binding; + public Function0 datePickerButtonClickListener = () -> null; + + + public CustomMessageComposerLeadingContent(@NonNull Context context) { + this(context, null); + } + + public CustomMessageComposerLeadingContent(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomMessageComposerLeadingContent(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + binding = MessageComposerLeadingContentBinding.inflate(LayoutInflater.from(context), this, true); + binding.datePickerButton.setOnClickListener((view) -> { + datePickerButtonClickListener.invoke(); + }); + } + + @Override + public void attachContext(@NonNull MessageComposerContext messageComposerContext) { + // Access the style if necessary + MessageComposerViewStyle style = messageComposerContext.getStyle(); + } + + @Override + public void renderState(@NonNull MessageComposerState state) { + // Render the state of the component + } +} +``` + + + + + + +```kotlin +val leadingContent = CustomMessageComposerLeadingContent(context).also { + it.datePickerButtonClickListener = { + // Create an instance of a date picker dialog + val datePickerDialog = MaterialDatePicker.Builder + .datePicker() + .build() + + datePickerDialog.addOnPositiveButtonClickListener { + // Handle date selection + } + + // Show the date picker dialog + datePickerDialog.show(supportFragmentManager, null) + } +} + +messageComposerView.setLeadingContent(leadingContent) +``` + + + + +```java +// Create an instance of a date picker dialog +MaterialDatePicker datePickerDialog = MaterialDatePicker.Builder.datePicker().build(); +datePickerDialog.addOnPositiveButtonClickListener(selection -> { + // Handle date selection +}); + +CustomMessageComposerLeadingContent leadingContent = new CustomMessageComposerLeadingContent(context); +leadingContent.datePickerButtonClickListener = () -> { + // Show the date picker dialog + datePickerDialog.show(supportFragmentManager, null); + return Unit.INSTANCE; +}; + +messageComposerView.setLeadingContent(leadingContent); +``` + + + +In the example above, we inflated a simple layout with a single date picker button and defined a listener to handle clicks on the button. + +| ![Message Composer](../../assets/message_composer_custom_slot_1.png) | ![Message Composer](../../assets/message_composer_custom_slot_2.png) | +|----------------------------------------------------------------------|----------------------------------------------------------------------| + +Date selection is not handled in this example for the sake of being concise. If you want to learn how to send a message with the selected date, consider reading the detailed guide on [Custom Attachments](04-custom-attachments.mdx). diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/02-working-with-attachments.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/02-working-with-attachments.mdx new file mode 100644 index 00000000000..c1fb75c20f6 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/02-working-with-attachments.mdx @@ -0,0 +1,283 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Working with Attachments/Files + +The SDK allows users to add attachments to a message. Several attachment types are supported, such as image, video, file and Giphy. + +## Attachment Gallery + +`AttachmentGalleryActivity` is an Activity used to display image and video attachments that the users have sent in the chat. Users can view, share, and download the attachments, and use an overview to easily navigate through them. + +| Light Mode | Dark Mode | +| --- | --- | +|![Attachment gallery example 1 light](../../assets/attachment_gallery_example1_light.png)|![Attachment gallery example 2 dark](../../assets/attachment_gallery_example1_dark.png)| +|![Attachment gallery example 1 light](../../assets/attachment_gallery_example2_light.png)|![Attachment gallery example 2 dark](../../assets/attachment_gallery_example2_dark.png)| + +### Handling Actions + +There are four user actions that can be customized by the following handlers: + +- `AttachmentReplyOptionHandler` +- `AttachmentShowInChatOptionHandler` +- `AttachmentDownloadOptionHandler` +- `AttachmentDeleteOptionHandler` + +These are called when the user selects one of the options from the overflow menu in the top right corner: + +|Light|Dark| +|---|---| +|![Attachment gallery example light](../../assets/attachment_activity_menu_light.png)|![Attachment gallery example dark](../../assets/attachment_activity_menu_dark.png)| + +As the gallery is usually opened from `MessageListView`, you can set these handlers on the View in the following way: + + + + +```kotlin +messageListView.setAttachmentReplyOptionClickHandler { resultItem -> + resultItem.messageId + // Handle reply to attachment +} + +messageListView.setAttachmentShowInChatOptionClickHandler { resultItem -> + resultItem.messageId + // Handle show in chat +} + +messageListView.setDownloadOptionHandler { resultItem -> + resultItem.assetUrl + // Handle download the attachment +} + +messageListView.setAttachmentDeleteOptionClickHandler { resultItem -> + resultItem.assetUrl + resultItem.imageUrl + // Handle delete +} +``` + + + + +```java +messageListView.setAttachmentReplyOptionClickHandler(resultItem -> { + resultItem.getMessageId(); + // Handle reply to attachment +}); + +messageListView.setAttachmentShowInChatOptionClickHandler(resultItem -> { + resultItem.getMessageId(); + // Handle show in chat +}); + +messageListView.setDownloadOptionHandler(resultItem -> { + resultItem.getAssetUrl(); + // Handle download the attachment +}); + +messageListView.setAttachmentDeleteOptionClickHandler(resultItem -> { + resultItem.getAssetUrl(); + resultItem.getImageUrl(); + // Handle delete +}); +``` + + + +### Navigating to the Attachment Gallery + +By default, the Attachment Gallery is opened when a user clicks on an attachment in `MessageListView`. In which case, all actions mentioned above have a default implementation, which can be changed by overriding `MessageListView`'s handlers. + +You can also navigate to `AttachmentGalleryActivity` manually from anywhere else in your code. In this case, you will need to implement all available actions: + + + + +```kotlin +// Create Attachment Gallery Destination +val destination = AttachmentGalleryDestination( + requireContext(), + attachmentReplyOptionHandler = { resultItem -> + // Handle reply + }, + attachmentShowInChatOptionHandler = { resultItem -> + // Handle show image in chat + }, + attachmentDownloadOptionHandler = { resultItem -> + // Handle download image + }, + attachmentDeleteOptionClickHandler = { resultItem -> + // Handle delete image + }, +) + +// Register destination with the ActivityResultRegistry +activity?.activityResultRegistry?.let { registry -> + destination.register(registry) +} + +// Set the data to display +destination.setData(attachmentGalleryItems = listOf(), attachmentIndex = 0) + +// Fire the navigation request +ChatUI.navigator.navigate(destination) +``` + + + + +```java +// Create Attachment Gallery Destination +AttachmentGalleryDestination destination = new AttachmentGalleryDestination( + activity, + resultItem -> { + // Handle reply + }, + resultItem -> { + // Handle show image in chat + }, + resultItem -> { + // Handle download image + }, + resultItem -> { + // Handle delete image + } +); + +// Register destination with the ActivityResultRegistry +destination.register(activity.getActivityResultRegistry()); + +// Set the data to display +int attachmentIndex = 0; +destination.setData(Collections.emptyList(), attachmentIndex); + +// Fire the navigation request +ChatUI.getNavigator().navigate(destination); +``` + + + +### Customization + +The gallery can be customized through the use of styleable attributes. These are organized into individual styles which apply to a certain aspect or feature of the gallery. + +You can find all of the styles listed [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_attachment_gallery_activity.xml).
+Some of these styles are applied directly to stock Android components. These accept any attribute that is normally applicable to the given stock Android component. Others power Stream UI components and have custom made styled attributes. + +- `streamUiAttachmentGalleryCloseButtonStyle`: Styles the button used to close the gallery. Accepts all `ImageView` attributes. +- `streamUiAttachmentGalleryTitleStyle`: Styles the title seen in the header. Accepts all `TextView` attributes. +- `streamUiAttachmentGalleryDateStyle`: Styles the date section in the header. Accepts all `TextView` attributes. +- `streamUiAttachmentGalleryActionsMenuStyle`: Styles the button used to activate the options menu. Accepts all `ImageView` attributes. +- `streamUiAttachmentGalleryBottomBarImageCountStyle`: Styles the text displaying the number of all media attachments contained within the message and the position of the one currently being viewed. Accepts all `TextView` attributes. +- `streamUiAttachmentGalleryBottomBarLeftIconStyle`: Styles the left icon inside the bottom bar. By default it shows a share icon. Accepts all `ImageView` attributes. +- `streamUiAttachmentGalleryBottomBarRightIconStyle`: Styles the right icon inside the bottom bar. By default it shows a tiled icon that opens the menu. Accepts all `ImageView` attributes. +- `streamUiAttachmentGalleryOptionsStyle`: Styles the actions menu. Accepts custom Stream attributes, all of which are listed [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_attachment_options_view.xml). +- `streamUiAttachmentGalleryVideoAttachmentsStyle`: Styles how video attachments are displayed in the main viewing area of the gallery. Accepts custom Stream attributes, all of which are listed [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_attachment_gallery_video_attachments.xml). +- `streamUiMediaAttachmentGridViewStyle`: Styles how the media overview menu is displayed. Accepts custom Stream attributes, all of which are listed [here](https://github.com/GetStream/stream-chat-android/blob/main/stream-chat-android-ui-components/src/main/res/values/attrs_media_attachment_grid_view.xml). + +#### Creating styles to change the appearance + +Let's start customizing our screen by styling the play button displayed over videos so that it features a flat design. + +1. Create a style which changes the way the play button is displayed in the main viewing area of the gallery: + +```xml + +``` + +2. Create a style which changes the way the play button is displayed in the overview: + +```xml + +``` + +3. Use the newly created style to create a custom Stream theme: + +```xml + +``` + +4. Add the custom theme to your attachment gallery Activity theme, along with a few lines that make the status and navigation bars look prettier: + +```xml + +``` + +5. Finally, override the default gallery theme in your app's manifest: + +```xml + +``` + +These few simple steps result in the following UI: + +| Light Mode | Dark Mode | +| --- | --- | +|![Attachment gallery customization example light](../../assets/attachment_gallery_customization_example_light.png)|![Attachment gallery customization example dark](../../assets/attachment_gallery_customization_example_dark.png)| + +As you can see, we've replaced the elevated white circular play button shown in the first few screenshots with a flat black squircle. + +We can customize other aspects of the gallery, for instance by removing menu options which we want to restrict from the user.
+Let's remove the download and delete options: + +1. Create a style that disables the previously mentioned options: + +```xml + +``` + +2. Add it to the custom Stream theme we've created in the previous example: + +```xml + +``` + +Which gives the options menu the following appearance: + +|Light|Dark| +|---|---| +|![Attachment gallery customization example light](../../assets/attachment_gallery_customization_example_options_light.png)|![Attachment gallery customization example dark](../../assets/attachment_gallery_customization_example_options_dark.png)| + +Now you have the ability to make the gallery feel like an organic part of your app. diff --git a/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/03-custom-message-composer.mdx b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/03-custom-message-composer.mdx new file mode 100644 index 00000000000..75f4d2cece8 --- /dev/null +++ b/docusaurus/android_versioned_docs/version-draft/02-ui-components/06-message-composer/03-custom-message-composer.mdx @@ -0,0 +1,431 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Customizing Message Composer + +:::note +You can find the full code from this guide on [GitHub](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-guides/src/main/java/io/getstream/chat/android/guides/catalog/uicomponents/customcomposer). To check the final result, clone the repository, select the `stream-chat-android-ui-guides` module on your Android Studio like the image below, and run the module. ![UI Guides Module on Android Studio](../../assets/ui_guides_module_android_studio.png) +::: + +If the built-in [Message Composer View](01-message-composer.mdx) and its available customization options don't fit your app's needs, you can create a Message Composer View of your own. + +Note that the UI Components Message Composer View supports many advanced features that you'll otherwise have to implement yourself if you want to use them in your app: + +- Sending and editing messages +- Handling threads and replies +- Supporting typing indicators +- Browsing for and adding image and file attachments +- Input validation such as a max length +- Commands and mentions + +With that, let's see how you can build a custom Message Composer View from scratch. + +:::note +This sample is meant to be as simple as possible. You might want to architect your actual custom views in more advanced ways than shown here. +::: + +## Creating a Layout + +For this example, you'll create a custom View that extends `ConstraintLayout`. It'll inflate the following layout internally, which consists of a simple `EditText` and a `Button`. + +```xml + + + + + +