Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[i232] Support message decorators customizations #5144

Merged
merged 5 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
### ✅ Added

### ⚠️ Changed
- Exposed `Decorator` and related classes for the better `MessageListView` customization. [#5144](https://github.com/GetStream/stream-chat-android/pull/5144)

### ❌ Removed

Expand Down
217 changes: 217 additions & 0 deletions docusaurus/docs/Android/04-ui/02-general-customization/02-chatui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -734,3 +734,220 @@ Which makes the UI look like so:
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`:

<Tabs>
<TabItem value="kotlin" label="Kotlin">

```kotlin
ChatUI.decoratorProviderFactory = DecoratorProviderFactory.defaultFactory {
it.type != Decorator.Type.BuiltIn.REPLY
}
```
</TabItem>

<TabItem value="java" label="Java">

```java
ChatUI.setDecoratorProviderFactory(
DecoratorProviderFactory.defaultFactory(
decorator -> decorator.getType() != Decorator.Type.BuiltIn.REPLY
)
);
```
</TabItem>
</Tabs>


| 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<TextView>(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", "<channel_id>")

// 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()
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import io.getstream.chat.android.ui.feature.messages.composer.attachment.preview.AttachmentPreviewFactoryManager;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.QuotedAttachmentFactoryManager;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.DecoratorProviderFactory;
import io.getstream.chat.android.ui.font.ChatFonts;
import io.getstream.chat.android.ui.font.TextStyle;
import io.getstream.chat.android.ui.helper.SupportedReactions;
Expand All @@ -45,6 +47,7 @@
import io.getstream.chat.android.ui.widgets.avatar.UserAvatarRenderer;
import io.getstream.chat.android.ui.widgets.avatar.UserAvatarView;
import io.getstream.chat.docs.R;
import kotlin.jvm.functions.Function1;

/**
* [General Configuration](https://getstream.io/chat/docs/sdk/android/ui/general-customization/chatui/)
Expand Down Expand Up @@ -237,17 +240,17 @@ private void customizeQuotedMessageContent() {
}

/**
* [Disabling Video Thumbnails](https://getstream.io/chat/docs/sdk/android/ui/general-customization/chatui/#disabling-video-thumbnails)
*/
private void disablingVideoThumbnails(){
* [Disabling Video Thumbnails](https://getstream.io/chat/docs/sdk/android/ui/general-customization/chatui/#disabling-video-thumbnails)
*/
private void disablingVideoThumbnails() {

}

private void customizingUserAvatarRenderer() {
final UserAvatarRenderer renderer = new UserAvatarRenderer() {
@Override
public void render(
@NonNull AvatarStyle style,
@NonNull AvatarStyle style,
@NonNull User user,
@NonNull UserAvatarView target
) {
Expand Down Expand Up @@ -293,4 +296,12 @@ public void render(
};
ChatUI.setChannelAvatarRenderer(renderer);
}

private void customizingDefaultDecoratorProviderFactory() {
ChatUI.setDecoratorProviderFactory(
DecoratorProviderFactory.defaultFactory(
decorator -> decorator.getType() != Decorator.Type.BuiltIn.REPLY
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ID>LongMethod:RepliedMessagesComponentBrowserFragment.kt$RepliedMessagesComponentBrowserFragment$@OptIn(InternalStreamChatApi::class) override fun getItems(): List&lt;MessageListItem.MessageItem></ID>
<ID>LongParameterList:AddChannelViewController.kt$AddChannelViewController$( private val headerView: AddChannelHeader, private val usersTitle: TextView, private val usersRecyclerView: RecyclerView, private val createGroupContainer: ViewGroup, private val messageListView: MessageListView, private val messageComposerView: MessageComposerView, private val emptyStateView: View, private val loadingView: View, private val isAddGroupChannel: Boolean, )</ID>
<ID>LongParameterList:ConfirmationDialogFragment.kt$ConfirmationDialogFragment.Companion$( @DrawableRes iconResId: Int, @ColorRes iconTintResId: Int, title: String, description: String, confirmText: String, cancelText: String, hasConfirmButton: Boolean = true, )</ID>
<ID>LongParameterList:CustomDecoratorProvider.kt$CustomDecoratorProvider$( channel: Channel, dateFormatter: DateFormatter, messageListViewStyle: MessageListViewStyle, showAvatarPredicate: MessageListView.ShowAvatarPredicate, messageBackgroundFactory: MessageBackgroundFactory, deletedMessageVisibility: () -> DeletedMessageVisibility, getLanguageDisplayName: (code: String) -> String, )</ID>
<ID>MagicNumber:ChatFragment.kt$ChatFragment$4</ID>
<ID>MagicNumber:ComponentBrowserHomeFragment.kt$ComponentBrowserHomeFragment$10</ID>
<ID>MagicNumber:ComponentBrowserHomeFragment.kt$ComponentBrowserHomeFragment$20</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFacto
import io.getstream.chat.android.state.plugin.config.StatePluginConfig
import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory
import io.getstream.chat.android.ui.ChatUI
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.DecoratorProviderFactory
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.plus
import io.getstream.chat.ui.sample.BuildConfig
import io.getstream.chat.ui.sample.debugger.CustomChatClientDebugger
import io.getstream.chat.ui.sample.feature.HostActivity
import io.getstream.chat.ui.sample.feature.chat.messagelist.decorator.CustomDecoratorProviderFactory

class ChatInitializer(
private val context: Context,
Expand Down Expand Up @@ -132,5 +135,7 @@ class ChatInitializer(
// audioRecordingSlideToCancelText = "Wash to cancel",
// )
// }

ChatUI.decoratorProviderFactory = CustomDecoratorProviderFactory() + DecoratorProviderFactory.defaultFactory()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.ui.sample.feature.chat.messagelist.decorator

import io.getstream.chat.android.models.Channel
import io.getstream.chat.android.ui.common.helper.DateFormatter
import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility
import io.getstream.chat.android.ui.feature.messages.list.MessageListView
import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.DecoratorProvider
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.DecoratorProviderFactory
import io.getstream.chat.android.ui.feature.messages.list.background.MessageBackgroundFactory

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,
)
}

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 {
listOf(ForwardedDecorator())
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014-2022 Stream.io Inc. All rights reserved.
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
Expand All @@ -14,8 +14,10 @@
* limitations under the License.
*/

package io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.internal
package io.getstream.chat.ui.sample.feature.chat.messagelist.decorator

internal interface DecoratorProvider {
val decorators: List<Decorator>
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.decorator.Decorator

enum class CustomDecoratorType : Decorator.Type {
FORWARDED,
}
Loading
Loading