diff --git a/CHANGELOG.md b/CHANGELOG.md index ead5376f6de..fe2e12603da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ ### ⬆️ Improved ### ✅ Added +- Add `ThreadListView` component for showing the list of threads for the user. [#5491](https://github.com/GetStream/stream-chat-android/pull/5491) ### ⚠️ Changed diff --git a/stream-chat-android-ai-assistant/src/main/kotlin/io/getstream/chat/android/ai/assistant/AiMessagesScreen.kt b/stream-chat-android-ai-assistant/src/main/kotlin/io/getstream/chat/android/ai/assistant/AiMessagesScreen.kt index 7b633ae4a5f..aea446a61a3 100644 --- a/stream-chat-android-ai-assistant/src/main/kotlin/io/getstream/chat/android/ai/assistant/AiMessagesScreen.kt +++ b/stream-chat-android-ai-assistant/src/main/kotlin/io/getstream/chat/android/ai/assistant/AiMessagesScreen.kt @@ -161,6 +161,7 @@ public fun AiMessagesScreen( val backAction: BackAction = remember(listViewModel, composerViewModel, attachmentsPickerViewModel) { { + val isStartedForThread = listViewModel.isStartedForThread val isInThread = listViewModel.isInThread val isShowingOverlay = listViewModel.isShowingOverlay @@ -171,6 +172,7 @@ public fun AiMessagesScreen( ) isShowingOverlay -> listViewModel.selectMessage(null) + isStartedForThread -> onBackPressed() isInThread -> { listViewModel.leaveThread() composerViewModel.leaveThread() diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 2ffe3cc7795..0af6b59ffa6 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -3670,6 +3670,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun isOnline ()Lkotlinx/coroutines/flow/Flow; public final fun isShowingOverlay ()Z public final fun isShowingPollOptionDetails ()Z + public final fun isStartedForThread ()Z public final fun leaveThread ()V public final fun loadNewerMessages (Ljava/lang/String;I)V public static synthetic fun loadNewerMessages$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Ljava/lang/String;IILjava/lang/Object;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index b851c6462d1..a0f0a0f958e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -183,6 +183,7 @@ public fun MessagesScreen( val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 val backAction: BackAction = remember(listViewModel, composerViewModel, attachmentsPickerViewModel) { { + val isStartedForThread = listViewModel.isStartedForThread val isInThread = listViewModel.isInThread val isShowingOverlay = listViewModel.isShowingOverlay @@ -193,6 +194,7 @@ public fun MessagesScreen( ) isShowingOverlay -> listViewModel.selectMessage(null) + isStartedForThread -> onBackPressed() isInThread -> { listViewModel.leaveThread() composerViewModel.leaveThread() diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt index 0e897edcede..fd4e3f40b83 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt @@ -106,6 +106,12 @@ public class MessageListViewModel( */ public val messageActions: Set by messageListController.messageActions.asState(viewModelScope) + /** + * Gives us information if the [MessageListViewModel] was started for the purpose of showing a thread. + */ + public val isStartedForThread: Boolean + get() = messageListController.isStartedForThread + /** * Gives us information if we're currently in the [Thread] message mode. */ diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 63e9d35d48a..f5012c1024b 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -241,7 +241,7 @@ View Comments - No threads here yet... + No threads here yet… " in " %d new thread diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 852ee665c68..edb59cd8453 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -172,6 +172,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; public final fun isInThread ()Z public final fun isInsideSearch ()Lkotlinx/coroutines/flow/StateFlow; + public final fun isStartedForThread ()Z public final fun loadMessageById (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public static synthetic fun loadMessageById$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun loadNewerMessages (Ljava/lang/String;I)V diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 37c159086fc..7ffa12e3d31 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -258,6 +258,11 @@ public class MessageListController( private val _mode: MutableStateFlow = MutableStateFlow(MessageMode.Normal) public val mode: StateFlow = _mode + /** + * Gives us information if the [MessageListController] was started for the purpose of showing a thread. + */ + public val isStartedForThread: Boolean = parentMessageId != null + /** * Gives us information if we're currently in the [MessageMode.MessageThread] mode. */ @@ -550,24 +555,30 @@ public class MessageListController( } } .onFirst { channelUserRead -> - val unreadMessages = (channelState.value?.messages?.value ?: emptyList()) - .fold(emptyList()) { acc, message -> - when { - channelUserRead.lastReadMessageId == message.id -> emptyList() - else -> acc + message + // Don't show the label if no unread messages in the channel, or the controller is started for a thread + val unreadLabel = if (isStartedForThread || channelUserRead.unreadMessages == 0) { + null + } else { + val unreadMessages = (channelState.value?.messages?.value ?: emptyList()) + .fold(emptyList()) { acc, message -> + when { + channelUserRead.lastReadMessageId == message.id -> emptyList() + else -> acc + message + } } - } - unreadLabelState.value = channelUserRead.lastReadMessageId - ?.takeUnless { unreadMessages.isEmpty() } - ?.takeUnless { unreadMessages.lastOrNull()?.id == it } - ?.let { lastReadMessageId -> - UnreadLabel( - unreadCount = channelUserRead.unreadMessages, - lastReadMessageId = lastReadMessageId, - buttonVisibility = shouldShowButton && - unreadMessages.any { !it.isDeleted() }, - ) - } + channelUserRead.lastReadMessageId + ?.takeUnless { unreadMessages.isEmpty() } + ?.takeUnless { unreadMessages.lastOrNull()?.id == it } + ?.let { lastReadMessageId -> + UnreadLabel( + unreadCount = channelUserRead.unreadMessages, + lastReadMessageId = lastReadMessageId, + buttonVisibility = shouldShowButton && + unreadMessages.any { !it.isDeleted() }, + ) + } + } + unreadLabelState.value = unreadLabel }.launchIn(scope) } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/channel/list/ChannelListFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/channel/list/ChannelListFragment.kt index 4dfdcf00cb3..7214e55edae 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/channel/list/ChannelListFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/channel/list/ChannelListFragment.kt @@ -147,7 +147,7 @@ class ChannelListFragment : Fragment() { binding.searchResultListView.setSearchResultSelectedListener { message -> requireActivity().findNavController(R.id.hostFragmentContainer) - .navigateSafely(HomeFragmentDirections.actionOpenChat(message.cid, message.id)) + .navigateSafely(HomeFragmentDirections.actionOpenChat(message.cid, message.id, message.parentId)) } } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeFragment.kt index 420218d8874..fd085325cd6 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeFragment.kt @@ -186,6 +186,10 @@ class HomeFragment : Fragment() { backgroundColor = ContextCompat.getColor(requireContext(), R.color.stream_ui_accent_red) badgeTextColor = ContextCompat.getColor(requireContext(), R.color.stream_ui_literal_white) } + getOrCreateBadge(R.id.threads_fragment).apply { + backgroundColor = ContextCompat.getColor(requireContext(), R.color.stream_ui_accent_red) + badgeTextColor = ContextCompat.getColor(requireContext(), R.color.stream_ui_literal_white) + } } } @@ -229,6 +233,7 @@ class HomeFragment : Fragment() { binding.bottomNavigationView.apply { setBadgeNumber(R.id.channels_fragment, state.totalUnreadCount) setBadgeNumber(R.id.mentions_fragment, state.mentionsUnreadCount) + setBadgeNumber(R.id.threads_fragment, state.unreadThreadsCount) } nameTextView.text = state.user.name diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeViewModel.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeViewModel.kt index 3f8bb061a62..de6fd6edd53 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeViewModel.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/home/HomeViewModel.kt @@ -73,11 +73,14 @@ class HomeViewModel( val events: LiveData> = _events init { - _state.postValue(initialState) + setState { initialState } _state.addSource(globalState.totalUnreadCount.asLiveData()) { count -> setState { copy(totalUnreadCount = count) } } + _state.addSource(globalState.unreadThreadsCount.asLiveData()) { count -> + setState { copy(unreadThreadsCount = count) } + } _state.addSource(clientState.user.asLiveData()) { user -> setState { copy(user = user ?: User()) } } @@ -119,11 +122,13 @@ class HomeViewModel( * @param user The currently logged in user. * @param totalUnreadCount The total unread messages count for the current user. * @param mentionsUnreadCount The number of unread mentions by the current user. + * @param unreadThreadsCount The number of unread threads by the current user. */ data class UiState( val user: User = User(), val totalUnreadCount: Int = 0, val mentionsUnreadCount: Int = 0, + val unreadThreadsCount: Int = 0, ) /** diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/mentions/MentionsFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/mentions/MentionsFragment.kt index 24367ffe159..abe6d195a3d 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/mentions/MentionsFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/mentions/MentionsFragment.kt @@ -56,7 +56,7 @@ class MentionsFragment : Fragment() { viewModel.bindView(binding.mentionsListView, viewLifecycleOwner) binding.mentionsListView.setMentionSelectedListener { message -> requireActivity().findNavController(R.id.hostFragmentContainer) - .navigateSafely(HomeFragmentDirections.actionOpenChat(message.cid, message.id)) + .navigateSafely(HomeFragmentDirections.actionOpenChat(message.cid, message.id, message.parentId)) } setupOnClickListeners() } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/threads/ThreadsFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/threads/ThreadsFragment.kt new file mode 100644 index 00000000000..eadbfdf0938 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/threads/ThreadsFragment.kt @@ -0,0 +1,78 @@ +/* + * 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.threads + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.addCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.findNavController +import io.getstream.chat.android.ui.viewmodel.threads.ThreadListViewModel +import io.getstream.chat.android.ui.viewmodel.threads.ThreadsViewModelFactory +import io.getstream.chat.android.ui.viewmodel.threads.bindView +import io.getstream.chat.ui.sample.R +import io.getstream.chat.ui.sample.common.navigateSafely +import io.getstream.chat.ui.sample.databinding.FragmentThreadsBinding +import io.getstream.chat.ui.sample.feature.home.HomeFragmentDirections + +/** + * Fragment displaying the list of threads for the currently logged in user. + */ +class ThreadsFragment : Fragment() { + + private var _binding: FragmentThreadsBinding? = null + private val binding: FragmentThreadsBinding + get() = _binding!! + + private val viewModel: ThreadListViewModel by viewModels { ThreadsViewModelFactory() } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = FragmentThreadsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.bindView(binding.threadListView, viewLifecycleOwner) + binding.threadListView.setThreadClickListener { thread -> + requireActivity().findNavController(R.id.hostFragmentContainer) + .navigateSafely( + HomeFragmentDirections.actionOpenChat( + cid = thread.parentMessage.cid, + parentMessageId = thread.parentMessageId, + ), + ) + } + setupBackHandler() + } + + private fun setupBackHandler() { + activity?.apply { + onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + finish() + } + } + } +} diff --git a/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_threads.xml b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_threads.xml new file mode 100644 index 00000000000..0a6c48ebee4 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_threads.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_threads.xml b/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_threads.xml new file mode 100644 index 00000000000..a3b35a8c4b9 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_threads.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components-sample/src/main/res/menu/menu_bottom_navigation.xml b/stream-chat-android-ui-components-sample/src/main/res/menu/menu_bottom_navigation.xml index da419a96a13..24cedaf6352 100644 --- a/stream-chat-android-ui-components-sample/src/main/res/menu/menu_bottom_navigation.xml +++ b/stream-chat-android-ui-components-sample/src/main/res/menu/menu_bottom_navigation.xml @@ -25,4 +25,9 @@ android:icon="@drawable/ic_mentions" android:title="@string/home_bottom_nav_mentions" /> + diff --git a/stream-chat-android-ui-components-sample/src/main/res/navigation/home_nav_graph.xml b/stream-chat-android-ui-components-sample/src/main/res/navigation/home_nav_graph.xml index 77a9c75665d..d94a877f12b 100644 --- a/stream-chat-android-ui-components-sample/src/main/res/navigation/home_nav_graph.xml +++ b/stream-chat-android-ui-components-sample/src/main/res/navigation/home_nav_graph.xml @@ -35,4 +35,12 @@ android:label="MentionsFragment" tools:layout="@layout/fragment_mentions" /> + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/values/strings.xml b/stream-chat-android-ui-components-sample/src/main/res/values/strings.xml index c944804eda2..b064b677711 100644 --- a/stream-chat-android-ui-components-sample/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-components-sample/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ Sign Out Chats Mentions + Threads Let\'s start chatting! diff --git a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index 3f54f91fa73..ccaa34d5bc0 100644 --- a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api +++ b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api @@ -2889,13 +2889,14 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/Me public final class io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff { public static final field Companion Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff$Companion; - public fun (ZZZZZZZZZZZZZ)V + public fun (ZZZZZZZZZZZZZZ)V public final fun anyChanged ()Z public final fun component1 ()Z public final fun component10 ()Z public final fun component11 ()Z public final fun component12 ()Z public final fun component13 ()Z + public final fun component14 ()Z public final fun component2 ()Z public final fun component3 ()Z public final fun component4 ()Z @@ -2904,8 +2905,8 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/Me public final fun component7 ()Z public final fun component8 ()Z public final fun component9 ()Z - public final fun copy (ZZZZZZZZZZZZZ)Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff;ZZZZZZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff; + public final fun copy (ZZZZZZZZZZZZZZ)Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff;ZZZZZZZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff; public fun equals (Ljava/lang/Object;)Z public final fun getAttachments ()Z public final fun getDeleted ()Z @@ -2919,6 +2920,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/Me public final fun getReplyText ()Z public final fun getSyncStatus ()Z public final fun getText ()Z + public final fun getThreadMode ()Z public final fun getUser ()Z public fun hashCode ()I public final fun plus (Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff; @@ -3771,6 +3773,127 @@ public final class io/getstream/chat/android/ui/feature/search/list/SearchResult public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/feature/threads/list/ThreadListView : androidx/constraintlayout/widget/ConstraintLayout { + public fun (Landroid/content/Context;)V + public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V + public final fun setLoadMoreListener (Lio/getstream/chat/android/ui/feature/threads/list/ThreadListView$LoadMoreListener;)V + public final fun setThreadClickListener (Lio/getstream/chat/android/ui/feature/threads/list/ThreadListView$ThreadClickListener;)V + public final fun setUnreadThreadsBannerClickListener (Lio/getstream/chat/android/ui/feature/threads/list/ThreadListView$UnreadThreadsBannerClickListener;)V + public final fun setViewHolderFactory (Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewHolderFactory;)V + public final fun showLoading ()V + public final fun showThreads (Ljava/util/List;Z)V + public final fun showUnreadThreadsBanner (I)V +} + +public abstract interface class io/getstream/chat/android/ui/feature/threads/list/ThreadListView$LoadMoreListener { + public abstract fun onLoadMore ()V +} + +public abstract interface class io/getstream/chat/android/ui/feature/threads/list/ThreadListView$ThreadClickListener { + public abstract fun onThreadClick (Lio/getstream/chat/android/models/Thread;)V +} + +public abstract interface class io/getstream/chat/android/ui/feature/threads/list/ThreadListView$UnreadThreadsBannerClickListener { + public abstract fun onUnreadThreadsBannerClick ()V +} + +public final class io/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle : io/getstream/chat/android/ui/helper/ViewStyle { + public fun (ILandroid/graphics/drawable/Drawable;Ljava/lang/String;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/feature/messages/preview/MessagePreviewStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIIIIIII)V + public final fun component1 ()I + public final fun component10 ()Landroid/graphics/drawable/Drawable; + public final fun component11 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component12 ()Landroid/graphics/drawable/Drawable; + public final fun component13 ()Landroid/graphics/drawable/Drawable; + public final fun component14 ()I + public final fun component15 ()I + public final fun component16 ()I + public final fun component17 ()I + public final fun component18 ()I + public final fun component19 ()I + public final fun component2 ()Landroid/graphics/drawable/Drawable; + public final fun component20 ()I + public final fun component21 ()I + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component5 ()Landroid/graphics/drawable/Drawable; + public final fun component6 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component7 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component8 ()Lio/getstream/chat/android/ui/feature/messages/preview/MessagePreviewStyle; + public final fun component9 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun copy (ILandroid/graphics/drawable/Drawable;Ljava/lang/String;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/feature/messages/preview/MessagePreviewStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIIIIIII)Lio/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle;ILandroid/graphics/drawable/Drawable;Ljava/lang/String;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/feature/messages/preview/MessagePreviewStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIIIIIIIILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle; + public fun equals (Ljava/lang/Object;)Z + public final fun getBackgroundColor ()I + public final fun getBannerBackground ()Landroid/graphics/drawable/Drawable; + public final fun getBannerIcon ()Landroid/graphics/drawable/Drawable; + public final fun getBannerMarginBottom ()I + public final fun getBannerMarginLeft ()I + public final fun getBannerMarginRight ()I + public final fun getBannerMarginTop ()I + public final fun getBannerPaddingBottom ()I + public final fun getBannerPaddingLeft ()I + public final fun getBannerPaddingRight ()I + public final fun getBannerPaddingTop ()I + public final fun getBannerTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getEmptyStateDrawable ()Landroid/graphics/drawable/Drawable; + public final fun getEmptyStateText ()Ljava/lang/String; + public final fun getEmptyStateTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getLatestReplyStyle ()Lio/getstream/chat/android/ui/feature/messages/preview/MessagePreviewStyle; + public final fun getThreadIconDrawable ()Landroid/graphics/drawable/Drawable; + public final fun getThreadReplyToStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getThreadTitleStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getUnreadCountBadgeBackground ()Landroid/graphics/drawable/Drawable; + public final fun getUnreadCountBadgeTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem { + public abstract fun getStableId ()J +} + +public final class io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$LoadingMoreItem : io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem { + public static final field INSTANCE Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$LoadingMoreItem; + public fun equals (Ljava/lang/Object;)Z + public fun getStableId ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$ThreadItem : io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem { + public fun (Lio/getstream/chat/android/models/Thread;)V + public final fun component1 ()Lio/getstream/chat/android/models/Thread; + public final fun copy (Lio/getstream/chat/android/models/Thread;)Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$ThreadItem; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$ThreadItem;Lio/getstream/chat/android/models/Thread;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem$ThreadItem; + public fun equals (Ljava/lang/Object;)Z + public fun getStableId ()J + public final fun getThread ()Lio/getstream/chat/android/models/Thread; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewHolderFactory { + public fun ()V + protected fun createLoadingMoreViewHolder (Landroid/view/ViewGroup;)Lio/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder; + protected fun createThreadItemViewHolder (Landroid/view/ViewGroup;)Lio/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder; + public fun createViewHolder (Landroid/view/ViewGroup;I)Lio/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder; + protected final fun getClickListener ()Lio/getstream/chat/android/ui/feature/threads/list/ThreadListView$ThreadClickListener; + public fun getItemViewType (Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem;)I + public fun getItemViewType (Lio/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder;)I + protected final fun getStyle ()Lio/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle; +} + +public final class io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewType { + public static final field INSTANCE Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewType; + public static final field ITEM_LOADING_MORE I + public static final field ITEM_THREAD I +} + +public abstract class io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder { + public fun (Landroid/view/View;)V + public abstract fun bind (Lio/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem;)V +} + public abstract interface class io/getstream/chat/android/ui/font/ChatFonts { public abstract fun getFont (Lio/getstream/chat/android/ui/font/TextStyle;)Landroid/graphics/Typeface; public abstract fun setFont (Lio/getstream/chat/android/ui/font/TextStyle;Landroid/widget/TextView;)V @@ -3915,6 +4038,7 @@ public final class io/getstream/chat/android/ui/helper/TransformStyle { public static final fun getSearchInputViewStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; public static final fun getSearchResultListViewStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; public static final fun getSingleReactionViewStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; + public static final fun getThreadListViewStyle ()Lio/getstream/chat/android/ui/helper/StyleTransformer; public static final fun getTypingIndicatorViewStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; public static final fun getUnreadLabelButtonStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; public static final fun getUnsupportedAttachmentStyleTransformer ()Lio/getstream/chat/android/ui/helper/StyleTransformer; @@ -3945,6 +4069,7 @@ public final class io/getstream/chat/android/ui/helper/TransformStyle { public static final fun setSearchInputViewStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V public static final fun setSearchResultListViewStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V public static final fun setSingleReactionViewStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V + public static final fun setThreadListViewStyle (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V public static final fun setTypingIndicatorViewStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V public static final fun setUnreadLabelButtonStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V public static final fun setUnsupportedAttachmentStyleTransformer (Lio/getstream/chat/android/ui/helper/StyleTransformer;)V @@ -4939,6 +5064,24 @@ public final class io/getstream/chat/android/ui/viewmodel/search/SearchViewModel public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/search/SearchViewModel;Lio/getstream/chat/android/ui/feature/search/list/SearchResultListView;Landroidx/lifecycle/LifecycleOwner;)V } +public final class io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModel : androidx/lifecycle/ViewModel { + public fun (Lio/getstream/chat/android/ui/common/feature/threads/ThreadListController;)V + public final fun getState ()Landroidx/lifecycle/LiveData; + public final fun load ()V + public final fun loadNextPage ()V +} + +public final class io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModelBinding { + public static final fun bind (Lio/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModel;Lio/getstream/chat/android/ui/feature/threads/list/ThreadListView;Landroidx/lifecycle/LifecycleOwner;)V +} + +public final class io/getstream/chat/android/ui/viewmodel/threads/ThreadsViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { + public fun ()V + public fun (III)V + public synthetic fun (IIIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; +} + public final class io/getstream/chat/android/ui/viewmodel/typing/TypingIndicatorViewModel : androidx/lifecycle/ViewModel { public fun (Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff.kt index 435818f7d50..faf211c308f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff.kt @@ -30,6 +30,7 @@ public data class MessageListItemPayloadDiff( val mentions: Boolean, val footer: Boolean, val poll: Boolean, + val threadMode: Boolean, ) { public operator fun plus(other: MessageListItemPayloadDiff): MessageListItemPayloadDiff { return MessageListItemPayloadDiff( @@ -46,6 +47,7 @@ public data class MessageListItemPayloadDiff( mentions = mentions || other.mentions, footer = footer || other.footer, poll = poll || other.poll, + threadMode = threadMode || other.threadMode, ) } @@ -66,6 +68,7 @@ public data class MessageListItemPayloadDiff( mentions = false, footer = false, poll = false, + threadMode = false, ) public val FULL: MessageListItemPayloadDiff = MessageListItemPayloadDiff( @@ -82,6 +85,7 @@ public data class MessageListItemPayloadDiff( mentions = true, footer = true, poll = true, + threadMode = true, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/internal/MessageListItemDiffCallback.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/internal/MessageListItemDiffCallback.kt index 901ad254368..62b045d6034 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/internal/MessageListItemDiffCallback.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/internal/MessageListItemDiffCallback.kt @@ -77,6 +77,7 @@ internal object MessageListItemDiffCallback : DiffUtil.ItemCallback, isLoadingMore: Boolean) { + val isCurrentlyEmpty = requireAdapter().itemCount == 0 + val hasThreads = threads.isNotEmpty() + + binding.threadListRecyclerView.isVisible = hasThreads + binding.emptyContainer.isVisible = !hasThreads + binding.progressBar.isVisible = false + + val threadItems = threads.map(ThreadListItem::ThreadItem) + val loadingMoreItems = if (isLoadingMore) listOf(ThreadListItem.LoadingMoreItem) else emptyList() + requireAdapter().submitList(threadItems + loadingMoreItems) + + scrollListener.enablePagination() + + if (isCurrentlyEmpty && hasThreads) { + // Data is fully reloaded, ensure list is scrolled to the top + binding.threadListRecyclerView.scrollToPosition(0) + } + } + + /** + * Shows the loading state of the thread list. + */ + public fun showLoading() { + requireAdapter().submitList(emptyList()) // clear current list + binding.threadListRecyclerView.isVisible = false + binding.emptyContainer.isVisible = false + binding.progressBar.isVisible = true + scrollListener.disablePagination() + } + + /** + * Show the 'unread threads' banner. + * Hides the banner if [unreadThreadsCount] == 0. + * + * @param unreadThreadsCount The number of unread threads. + */ + public fun showUnreadThreadsBanner(unreadThreadsCount: Int) { + val bannerText = context.resources.getQuantityString( + R.plurals.stream_ui_thread_list_new_threads, + unreadThreadsCount, + unreadThreadsCount, + ) + binding.unreadThreadsBannerTextView.isVisible = unreadThreadsCount > 0 + binding.unreadThreadsBannerTextView.text = bannerText + } + + /** + * Sets the [ThreadListItemViewHolderFactory] used to create the thread list view holders. + * Use if you want completely custom views for the thread list items. + * Make sure to call this before setting/updating the data in the thread list view. + * + * @param factory The [ThreadListItemViewHolderFactory] to be used for creating the item view holders. + * @throws IllegalStateException if called when a [factory] was already set. + */ + public fun setViewHolderFactory(factory: ThreadListItemViewHolderFactory) { + check(::adapter.isInitialized.not()) { + "Adapter was already initialized, please set ChannelListItemViewHolderFactory first" + } + viewHolderFactory = factory + } + + /** + * Sets the listener for clicks on the unread threads banner. + * + * @param listener The [UnreadThreadsBannerClickListener] to be invoked when the user clicks on the unread threads + * banner. + */ + public fun setUnreadThreadsBannerClickListener(listener: UnreadThreadsBannerClickListener) { + binding.unreadThreadsBannerTextView.setOnClickListener { + listener.onUnreadThreadsBannerClick() + } + } + + /** + * Sets the listener for clicks on threads. + * + * @param listener The [ThreadClickListener] to be invoked when the user clicks on a thread. + */ + public fun setThreadClickListener(listener: ThreadClickListener) { + this.clickListener = listener + } + + /** + * Sets the listener requesting loading of more threads. + * + * @param listener The [LoadMoreListener] to be invoked when the end of the thread list is reached. + */ + public fun setLoadMoreListener(listener: LoadMoreListener) { + this.loadMoreListener = listener + } + + /** + * Ensures the [adapter] is initialized before accessing it. + * Useful for cases where a custom [viewHolderFactory] is provided. + */ + private fun requireAdapter(): ThreadListAdapter { + if (::adapter.isInitialized.not()) { + initAdapter() + } + return adapter + } + + private fun initAdapter() { + // Ensure the viewHolderFactory is initialized + if (::viewHolderFactory.isInitialized.not()) { + viewHolderFactory = ThreadListItemViewHolderFactory() + } + viewHolderFactory.setStyle(style) + viewHolderFactory.setThreadClickListener(clickListener) + adapter = ThreadListAdapter(style, viewHolderFactory) + binding.threadListRecyclerView.adapter = adapter + } + + private fun applyEmptyStateStyle(style: ThreadListViewStyle) { + binding.emptyImage.setImageDrawable(style.emptyStateDrawable) + binding.emptyTextView.text = style.emptyStateText + binding.emptyTextView.setTextStyle(style.emptyStateTextStyle) + } + + private fun applyBannerStyle(style: ThreadListViewStyle) { + binding.unreadThreadsBannerTextView.setTextStyle(style.bannerTextStyle) + binding.unreadThreadsBannerTextView.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, + null, + style.bannerIcon, + null, + ) + binding.unreadThreadsBannerTextView.background = style.bannerBackground + binding.unreadThreadsBannerTextView.updatePadding( + left = style.bannerPaddingLeft, + top = style.bannerPaddingTop, + right = style.bannerPaddingRight, + bottom = style.bannerPaddingBottom, + ) + binding.unreadThreadsBannerTextView.updateLayoutParams { + leftMargin = style.bannerMarginLeft + topMargin = style.bannerMarginTop + rightMargin = style.bannerMarginRight + bottomMargin = style.bannerMarginBottom + } + } + + private companion object { + private const val LOAD_MORE_THRESHOLD = 10 + } + + /** + * Listener for clicks on the "unread threads" banner. + */ + public fun interface UnreadThreadsBannerClickListener { + + /** + * Called when the user clicks on the unread threads banner. + */ + public fun onUnreadThreadsBannerClick() + } + + /** + * Listener for clicks on the thread list. + */ + public fun interface ThreadClickListener { + + /** + * Called when the user clicks on a thread in the list. + * + * @param thread The clicked [Thread]. + */ + public fun onThreadClick(thread: Thread) + } + + /** + * Listener invoked when the end of the thread list is reached, and a new page of threads should be loaded. + */ + public fun interface LoadMoreListener { + + /** + * Called when a new page of threads should be loaded. + */ + public fun onLoadMore() + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle.kt new file mode 100644 index 00000000000..b364416681f --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/ThreadListViewStyle.kt @@ -0,0 +1,374 @@ +/* + * 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.android.ui.feature.threads.list + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.annotation.Px +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.feature.messages.preview.MessagePreviewStyle +import io.getstream.chat.android.ui.font.TextStyle +import io.getstream.chat.android.ui.helper.TransformStyle +import io.getstream.chat.android.ui.helper.ViewStyle +import io.getstream.chat.android.ui.utils.extensions.getColorCompat +import io.getstream.chat.android.ui.utils.extensions.getDimension +import io.getstream.chat.android.ui.utils.extensions.getDrawableCompat +import io.getstream.chat.android.ui.utils.extensions.use + +/** + * Class holding the customizable styling parameters for [ThreadListView]. + * + * @param backgroundColor The background color of the thread list. + * @param emptyStateDrawable Drawable shown when there are no threads. + * @param emptyStateText Text shown when there are no threads. + * @param emptyStateTextStyle Style for the empty state text. + * @param threadIconDrawable Drawable for the thread icon. + * @param threadTitleStyle Style for the thread title. + * @param threadReplyToStyle Style for the thread reply to text. + * @param latestReplyStyle Style for the latest reply message preview. + * @param unreadCountBadgeTextStyle Style for the unread count badge. + * @param unreadCountBadgeBackground Background for the unread count badge. + * @param bannerTextStyle Style for the unread threads banner text. + * @param bannerIcon Icon for the unread threads banner. + * @param bannerBackground Background for the unread threads banner. + * @param bannerPaddingLeft Left padding for the unread threads banner. + * @param bannerPaddingTop Top padding for the unread threads banner. + * @param bannerPaddingRight Right padding for the unread threads banner. + * @param bannerPaddingBottom Bottom padding for the unread threads banner. + * @param bannerMarginLeft Left margin for the unread threads banner. + * @param bannerMarginTop Top margin for the unread threads banner. + * @param bannerMarginRight Right margin for the unread threads banner. + * @param bannerMarginBottom Bottom margin for the unread threads banner. + */ +public data class ThreadListViewStyle( + // General + @ColorInt public val backgroundColor: Int, + // Empty state + public val emptyStateDrawable: Drawable, + public val emptyStateText: String, + public val emptyStateTextStyle: TextStyle, + // Results + public val threadIconDrawable: Drawable, + public val threadTitleStyle: TextStyle, + public val threadReplyToStyle: TextStyle, + public val latestReplyStyle: MessagePreviewStyle, + public val unreadCountBadgeTextStyle: TextStyle, + public val unreadCountBadgeBackground: Drawable, + // Unread threads banner + public val bannerTextStyle: TextStyle, + public val bannerIcon: Drawable, + public val bannerBackground: Drawable, + @Px public val bannerPaddingLeft: Int, + @Px public val bannerPaddingTop: Int, + @Px public val bannerPaddingRight: Int, + @Px public val bannerPaddingBottom: Int, + @Px public val bannerMarginLeft: Int, + @Px public val bannerMarginTop: Int, + @Px public val bannerMarginRight: Int, + @Px public val bannerMarginBottom: Int, +) : ViewStyle { + + @Suppress("TooManyFunctions") + internal companion object { + + /** + * Creates a [ThreadListViewStyle] from the declared XML properties. + */ + operator fun invoke(context: Context, attrs: AttributeSet?): ThreadListViewStyle { + context.obtainStyledAttributes( + attrs, + R.styleable.ThreadListView, + R.attr.streamUiThreadListStyle, + R.style.StreamUi_ThreadList, + ).use { typedArray -> + // General + val backgroundColor = backgroundColor(context, typedArray) + // Empty state + val emptyStateDrawable = emptyStateDrawable(context, typedArray) + val emptyStateText = emptyStateText(context, typedArray) + val emptyStateTextStyle = emptyStateTextStyle(context, typedArray) + // Results + val threadIconDrawable = threadIconDrawable(context, typedArray) + val threadTitleStyle = threadTitleStyle(context, typedArray) + val threadReplyToStyle = threadReplyToStyle(context, typedArray) + val latestReplySenderStyle = threadLatestReplySenderStyle(context, typedArray) + val latestReplyMessageStyle = threadLatestReplyMessageStyle(context, typedArray) + val latestReplyTimeStyle = threadLatestReplyTimeStyle(context, typedArray) + val unreadCountBadgeTextStyle = unreadCountBadgeTextStyle(context, typedArray) + val unreadCountBadgeBackground = unreadCountBadgeBackground(context, typedArray) + // Unread threads banner + val bannerTextStyle = bannerTextStyle(context, typedArray) + val bannerIcon = bannerIcon(context, typedArray) + val bannerBackground = bannerBackground(context, typedArray) + val bannerPaddingLeft = bannerPaddingLeft(context, typedArray) + val bannerPaddingTop = bannerPaddingTop(context, typedArray) + val bannerPaddingRight = bannerPaddingRight(context, typedArray) + val bannerPaddingBottom = bannerPaddingBottom(context, typedArray) + val bannerMarginLeft = bannerMarginLeft(context, typedArray) + val bannerMarginTop = bannerMarginTop(context, typedArray) + val bannerMarginRight = bannerMarginRight(context, typedArray) + val bannerMarginBottom = bannerMarginBottom(context, typedArray) + return ThreadListViewStyle( + backgroundColor = backgroundColor, + emptyStateDrawable = emptyStateDrawable, + emptyStateText = emptyStateText, + emptyStateTextStyle = emptyStateTextStyle, + threadIconDrawable = threadIconDrawable, + threadTitleStyle = threadTitleStyle, + threadReplyToStyle = threadReplyToStyle, + latestReplyStyle = MessagePreviewStyle( + messageSenderTextStyle = latestReplySenderStyle, + messageTextStyle = latestReplyMessageStyle, + messageTimeTextStyle = latestReplyTimeStyle, + ), + unreadCountBadgeTextStyle = unreadCountBadgeTextStyle, + unreadCountBadgeBackground = unreadCountBadgeBackground, + bannerTextStyle = bannerTextStyle, + bannerIcon = bannerIcon, + bannerBackground = bannerBackground, + bannerPaddingLeft = bannerPaddingLeft, + bannerPaddingTop = bannerPaddingTop, + bannerPaddingRight = bannerPaddingRight, + bannerPaddingBottom = bannerPaddingBottom, + bannerMarginLeft = bannerMarginLeft, + bannerMarginTop = bannerMarginTop, + bannerMarginRight = bannerMarginRight, + bannerMarginBottom = bannerMarginBottom, + ).let(TransformStyle.threadListViewStyle::transform) + } + } + + private fun backgroundColor(context: Context, typedArray: TypedArray) = typedArray.getColor( + R.styleable.ThreadListView_streamUiThreadListBackground, + context.getColorCompat(R.color.stream_ui_white_snow), + ) + + private fun emptyStateDrawable(context: Context, typedArray: TypedArray) = + typedArray.getDrawable(R.styleable.ThreadListView_streamUiThreadListEmptyStateDrawable) + ?: context.getDrawableCompat(R.drawable.stream_ui_ic_threads_empty)!! + + private fun emptyStateText(context: Context, typedArray: TypedArray) = + typedArray.getString(R.styleable.ThreadListView_streamUiThreadListEmptyStateText) + ?: context.getString(R.string.stream_ui_thread_list_empty_title) + + private fun emptyStateTextStyle(context: Context, typedArray: TypedArray) = TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListEmptyStateTextSize, + context.getDimension(R.dimen.stream_ui_text_large), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListEmptyStateTextColor, + context.getColorCompat(R.color.stream_ui_text_color_secondary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListEmptyStateTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListEmptyStateTextFont, + ) + .style(R.styleable.ThreadListView_streamUiThreadListEmptyStateTextStyle, Typeface.NORMAL) + .build() + + private fun threadIconDrawable(context: Context, typedArray: TypedArray) = + typedArray.getDrawable(R.styleable.ThreadListView_streamUiThreadListThreadIconDrawable) + ?: context.getDrawableCompat(R.drawable.stream_ui_ic_thread)!! + + private fun threadTitleStyle(context: Context, typedArray: TypedArray) = TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadTitleTextSize, + context.getDimension(R.dimen.stream_ui_text_medium), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadTitleTextColor, + context.getColorCompat(R.color.stream_ui_text_color_primary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadTitleTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadTitleTextFont, + ) + .style(R.styleable.ThreadListView_streamUiThreadListThreadTitleTextStyle, Typeface.BOLD) + .build() + + private fun threadReplyToStyle(context: Context, typedArray: TypedArray) = TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadReplyToTextSize, + context.getDimension(R.dimen.stream_ui_text_medium), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadReplyToTextColor, + context.getColorCompat(R.color.stream_ui_text_color_secondary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadReplyToTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadReplyToTextFont, + ) + .style(R.styleable.ThreadListView_streamUiThreadListThreadReplyToTextStyle, Typeface.NORMAL) + .build() + + private fun threadLatestReplySenderStyle(context: Context, typedArray: TypedArray) = + TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplySenderTextSize, + context.getDimension(R.dimen.stream_ui_text_medium), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplySenderTextColor, + context.getColorCompat(R.color.stream_ui_text_color_primary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplySenderTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplySenderTextFont, + ) + .style( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplySenderTextStyle, + Typeface.BOLD, + ) + .build() + + private fun threadLatestReplyMessageStyle(context: Context, typedArray: TypedArray) = + TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyMessageTextSize, + context.getDimension(R.dimen.stream_ui_text_medium), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyMessageTextColor, + context.getColorCompat(R.color.stream_ui_text_color_secondary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyMessageTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyMessageTextFont, + ) + .style( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyMessageTextStyle, + Typeface.NORMAL, + ) + .build() + + private fun threadLatestReplyTimeStyle(context: Context, typedArray: TypedArray) = TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyTimeTextSize, + context.getDimension(R.dimen.stream_ui_text_medium), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyTimeTextColor, + context.getColorCompat(R.color.stream_ui_text_color_secondary), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyTimeTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyTimeTextFont, + ) + .style( + R.styleable.ThreadListView_streamUiThreadListThreadLatestReplyTimeTextStyle, + Typeface.NORMAL, + ) + .build() + + private fun unreadCountBadgeTextStyle(context: Context, typedArray: TypedArray) = TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeTextSize, + context.getDimension(R.dimen.stream_ui_text_small), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeTextColor, + context.getColorCompat(R.color.stream_ui_white), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeTextFont, + ) + .style( + R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeTextStyle, + Typeface.NORMAL, + ) + .build() + + private fun unreadCountBadgeBackground(context: Context, typedArray: TypedArray) = + typedArray.getDrawable(R.styleable.ThreadListView_streamUiThreadListThreadUnreadCountBadgeBackground) + ?: context.getDrawableCompat(R.drawable.stream_ui_shape_badge_background)!! + + private fun bannerTextStyle(context: Context, typedArray: TypedArray) = + TextStyle.Builder(typedArray) + .size( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerTextSize, + context.getDimension(R.dimen.stream_ui_text_large), + ) + .color( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerTextColor, + context.getColorCompat(R.color.stream_ui_white), + ) + .font( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerTextFontAssets, + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerTextFont, + ) + .style( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerTextStyle, + Typeface.NORMAL, + ) + .build() + + private fun bannerIcon(context: Context, typedArray: TypedArray) = + typedArray.getDrawable(R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerIcon) + ?: context.getDrawableCompat(R.drawable.stream_ui_ic_union)!! + + private fun bannerBackground(context: Context, typedArray: TypedArray) = + typedArray.getDrawable(R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerBackground) + ?: context.getDrawableCompat(R.drawable.stream_ui_shape_unread_threads_banner)!! + + private fun bannerPaddingLeft(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerPaddingLeft, + context.getDimension(R.dimen.stream_ui_spacing_medium), + ) + + private fun bannerPaddingTop(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerPaddingTop, + context.getDimension(R.dimen.stream_ui_spacing_medium), + ) + + private fun bannerPaddingRight(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerPaddingRight, + context.getDimension(R.dimen.stream_ui_spacing_medium), + ) + + private fun bannerPaddingBottom(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerPaddingBottom, + context.getDimension(R.dimen.stream_ui_spacing_medium), + ) + + private fun bannerMarginLeft(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerMarginLeft, + context.getDimension(R.dimen.stream_ui_spacing_small), + ) + + private fun bannerMarginTop(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerMarginTop, + context.getDimension(R.dimen.stream_ui_spacing_small), + ) + + private fun bannerMarginRight(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerMarginRight, + context.getDimension(R.dimen.stream_ui_spacing_small), + ) + + private fun bannerMarginBottom(context: Context, typedArray: TypedArray) = typedArray.getDimensionPixelSize( + R.styleable.ThreadListView_streamUiThreadListUnreadThreadsBannerMarginBottom, + context.getDimension(R.dimen.stream_ui_spacing_small), + ) + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem.kt new file mode 100644 index 00000000000..080930f1428 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItem.kt @@ -0,0 +1,51 @@ +/* + * 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.android.ui.feature.threads.list.adapter + +import io.getstream.chat.android.models.Thread + +/** + * Class representing the different types of items that can be rendered in the + * by the [io.getstream.chat.android.ui.feature.threads.list.adapter.internal.ThreadListAdapter]. + */ +public sealed interface ThreadListItem { + + /** + * The item stable ID. + */ + public val stableId: Long + + /** + * Represents a thread item. + */ + public data class ThreadItem(val thread: Thread) : ThreadListItem { + override val stableId: Long + get() = thread.parentMessage.identifierHash() + } + + /** + * Represents a loading more item. + */ + public data object LoadingMoreItem : ThreadListItem { + override val stableId: Long + get() = LOADING_MORE_ITEM_STABLE_ID + } + + private companion object { + private const val LOADING_MORE_ITEM_STABLE_ID = 1L + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewHolderFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewHolderFactory.kt new file mode 100644 index 00000000000..5d2a74b94a2 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewHolderFactory.kt @@ -0,0 +1,128 @@ +/* + * 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.android.ui.feature.threads.list.adapter + +import android.view.ViewGroup +import io.getstream.chat.android.ui.feature.threads.list.ThreadListView +import io.getstream.chat.android.ui.feature.threads.list.ThreadListViewStyle +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.BaseThreadListItemViewHolder +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.internal.ThreadItemViewHolder +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.internal.ThreadListLoadingMoreViewHolder + +/** + * Factory responsible for creating ViewHolder instances for the RecyclerView used in the + * [io.getstream.chat.android.ui.feature.threads.list.ThreadListView]. + */ +public open class ThreadListItemViewHolderFactory { + + /** + * The [ThreadListViewStyle] for styling the viewHolders. + */ + protected lateinit var style: ThreadListViewStyle + private set + + /** + * The listener for clicks on the thread items. + */ + protected var clickListener: ThreadListView.ThreadClickListener? = null + private set + + /** + * Returns a view type value based on the type of the given [item]. + * The view type returned here will be used as a parameter in [createViewHolder]. + * + * For built-in view types, see [ThreadListItemViewType] and its constants. + * + * @param item The [ThreadListItem] to check the type of. + */ + public open fun getItemViewType(item: ThreadListItem): Int { + return when (item) { + is ThreadListItem.ThreadItem -> ThreadListItemViewType.ITEM_THREAD + is ThreadListItem.LoadingMoreItem -> ThreadListItemViewType.ITEM_LOADING_MORE + } + } + + /** + * Returns a view type value based on the type of the given [viewHolder]. + * + * For built-in view types, see [ThreadListItemViewType] and its constants. + * + * @param viewHolder The [BaseThreadListItemViewHolder] to check the type of. + */ + public open fun getItemViewType(viewHolder: BaseThreadListItemViewHolder): Int { + return when (viewHolder) { + is ThreadItemViewHolder -> ThreadListItemViewType.ITEM_THREAD + is ThreadListLoadingMoreViewHolder -> ThreadListItemViewType.ITEM_LOADING_MORE + else -> throw IllegalArgumentException("Unhandled ThreadList view holder: $viewHolder") + } + } + + /** + * Creates a new ViewHolder based on the provided [viewType] to be used in the Thread List. + * The [viewType] parameter is determined by [getItemViewType]. + * + * @param parentView The parent of the view. + * @param viewType The type of the item for which the viewHolder is created. + */ + public open fun createViewHolder( + parentView: ViewGroup, + viewType: Int, + ): BaseThreadListItemViewHolder { + return when (viewType) { + ThreadListItemViewType.ITEM_THREAD -> createThreadItemViewHolder(parentView) + ThreadListItemViewType.ITEM_LOADING_MORE -> createLoadingMoreViewHolder(parentView) + else -> throw IllegalArgumentException("Unhandled ThreadList view type: $viewType") + } + } + + /** + * Creates the ViewHolder for the [ThreadListItemViewType.ITEM_THREAD] ([ThreadListItem.ThreadItem]) type. + * + * @param parentView The parent of the view. + */ + protected open fun createThreadItemViewHolder( + parentView: ViewGroup, + ): BaseThreadListItemViewHolder { + return ThreadItemViewHolder(parentView, style, clickListener) + } + + /** + * Creates the ViewHolder for the [ThreadListItemViewType.ITEM_LOADING_MORE] ([ThreadListItem.LoadingMoreItem]) + * type. + * + * @param parentView The parent of the view. + */ + protected open fun createLoadingMoreViewHolder( + parentView: ViewGroup, + ): BaseThreadListItemViewHolder { + return ThreadListLoadingMoreViewHolder(parentView) + } + + /** + * Sets the [ThreadListViewStyle] to be used by the created ViewHolders. + */ + internal fun setStyle(style: ThreadListViewStyle) { + this.style = style + } + + /** + * Sets the [ThreadListView.ThreadClickListener] for clicks on the thread items. + */ + internal fun setThreadClickListener(clickListener: ThreadListView.ThreadClickListener?) { + this.clickListener = clickListener + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewType.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewType.kt new file mode 100644 index 00000000000..617360ec5fd --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/ThreadListItemViewType.kt @@ -0,0 +1,33 @@ +/* + * 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.android.ui.feature.threads.list.adapter + +/** + * Defines the possible types of a Thread List item. + */ +public object ThreadListItemViewType { + + /** + * Represents an item of type 'thread'. + */ + public const val ITEM_THREAD: Int = 0 + + /** + * Represent a loading more indicator item. + */ + public const val ITEM_LOADING_MORE: Int = 1 +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/internal/ThreadListAdapter.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/internal/ThreadListAdapter.kt new file mode 100644 index 00000000000..016bf4924d8 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/internal/ThreadListAdapter.kt @@ -0,0 +1,88 @@ +/* + * 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.android.ui.feature.threads.list.adapter.internal + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import io.getstream.chat.android.ui.feature.threads.list.ThreadListViewStyle +import io.getstream.chat.android.ui.feature.threads.list.adapter.ThreadListItem +import io.getstream.chat.android.ui.feature.threads.list.adapter.ThreadListItemViewHolderFactory +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.BaseThreadListItemViewHolder +import io.getstream.log.taggedLogger + +/** + * RecyclerView adapter implementation for displaying a list of threads. + * + * @param style The [ThreadListViewStyle] for item customization. + * @param viewHolderFactory The factory for creating view holders. + */ +internal class ThreadListAdapter( + private val style: ThreadListViewStyle, + private val viewHolderFactory: ThreadListItemViewHolderFactory, +) : ListAdapter>(ThreadListItemDiffCallback) { + + private val logger by taggedLogger("Chat:ThreadListAdapter") + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return getItem(position).stableId + } + + override fun getItemViewType(position: Int): Int { + return viewHolderFactory.getItemViewType(getItem(position)) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): BaseThreadListItemViewHolder { + return viewHolderFactory.createViewHolder(parent, viewType) + } + + override fun onBindViewHolder(holder: BaseThreadListItemViewHolder, position: Int) { + val item = getItem(position) + val itemViewType = viewHolderFactory.getItemViewType(item) + val holderViewType = viewHolderFactory.getItemViewType(holder) + if (itemViewType != holderViewType) { + // Should never happen + logger.d { "Item view type $itemViewType does not match the holder view type $holderViewType" } + return + } + holder.bindInternal(item) + } + + /** + * [DiffUtil.ItemCallback] for calculating differences between [ThreadListItem]s. + */ + private object ThreadListItemDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ThreadListItem, newItem: ThreadListItem): Boolean { + return oldItem.stableId == newItem.stableId + } + + override fun areContentsTheSame(oldItem: ThreadListItem, newItem: ThreadListItem): Boolean { + return if (oldItem is ThreadListItem.ThreadItem && newItem is ThreadListItem.ThreadItem) { + oldItem.thread == newItem.thread // [Thread] is a data class, equality check is enough + } else { + false + } + } + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder.kt new file mode 100644 index 00000000000..ba94914f13f --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/BaseThreadListItemViewHolder.kt @@ -0,0 +1,53 @@ +/* + * 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.android.ui.feature.threads.list.adapter.viewholder + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import io.getstream.chat.android.ui.feature.threads.list.adapter.ThreadListItem +import io.getstream.log.taggedLogger + +/** + * Base ViewHolder used for displaying items by the + * [io.getstream.chat.android.ui.feature.threads.list.adapter.internal.ThreadListAdapter]. + */ +public abstract class BaseThreadListItemViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + + private val logger by taggedLogger("Chat:BaseThreadListItemViewHolder") + + /** + * Binds the item to the ViewHolder. + * + * @param item The item to bind. + */ + public abstract fun bind(item: T) + + /** + * Workaround to allow a downcast of the [ThreadListItem] to [T]. + */ + @Suppress("UNCHECKED_CAST") + internal fun bindInternal(item: ThreadListItem) { + val actual = item as? T + if (actual == null) { + // Should never happen + logger.d { "[bindInternal] Failed to cast $item to the expected type." } + return + } + bind(actual) + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadItemViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadItemViewHolder.kt new file mode 100644 index 00000000000..0b3e2db1f4b --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadItemViewHolder.kt @@ -0,0 +1,146 @@ +/* + * 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.android.ui.feature.threads.list.adapter.viewholder.internal + +import android.text.SpannableStringBuilder +import android.view.ViewGroup +import androidx.core.view.isVisible +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Thread +import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.ChatUI +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.databinding.StreamUiItemThreadListBinding +import io.getstream.chat.android.ui.feature.threads.list.ThreadListView +import io.getstream.chat.android.ui.feature.threads.list.ThreadListViewStyle +import io.getstream.chat.android.ui.feature.threads.list.adapter.ThreadListItem +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.BaseThreadListItemViewHolder +import io.getstream.chat.android.ui.font.setTextStyle +import io.getstream.chat.android.ui.utils.extensions.bold +import io.getstream.chat.android.ui.utils.extensions.getAttachmentsText +import io.getstream.chat.android.ui.utils.extensions.getTranslatedText +import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater + +/** + * Default ViewHolder for thread items. + * + * @param parentView The parent view group. + * @param style The style object for customizing the thread item appearance. + * @param clickListener The listener for item clicks. + * @param binding The view binding for the thread item. + */ +internal class ThreadItemViewHolder( + parentView: ViewGroup, + style: ThreadListViewStyle, + private val clickListener: ThreadListView.ThreadClickListener?, + private val binding: StreamUiItemThreadListBinding = StreamUiItemThreadListBinding.inflate( + parentView.streamThemeInflater, + parentView, + false, + ), +) : BaseThreadListItemViewHolder(binding.root) { + + private lateinit var thread: Thread + + init { + applyStyle(style) + binding.root.setOnClickListener { + clickListener?.onThreadClick(thread) + } + } + + override fun bind(item: ThreadListItem.ThreadItem) { + this.thread = item.thread + val currentUser = ChatUI.currentUserProvider.getCurrentUser() + bindThreadTitle(currentUser) + bindReplyTo() + bindUnreadCountBadge(currentUser) + bindLatestReply() + } + + private fun applyStyle(style: ThreadListViewStyle) { + binding.threadImage.setImageDrawable(style.threadIconDrawable) + binding.threadTitleTextView.setTextStyle(style.threadTitleStyle) + binding.replyToTextView.setTextStyle(style.threadReplyToStyle) + // Remove ripple from latestReplyMessageView + binding.latestReplyMessageView.binding.root.background = null + binding.latestReplyMessageView.styleView(style.latestReplyStyle) + binding.unreadCountBadge.setTextStyle(style.unreadCountBadgeTextStyle) + binding.unreadCountBadge.background = style.unreadCountBadgeBackground + } + + private fun bindThreadTitle(currentUser: User?) { + val channel = thread.channel + val title = channel + ?.let { ChatUI.channelNameFormatter.formatChannelName(channel = it, currentUser = currentUser) } + ?: thread.title + binding.threadTitleTextView.text = title + } + + private fun bindReplyTo() { + val prefix = binding.root.context.getString(R.string.stream_ui_thread_list_replied_to) + val parentMessageText = formatMessage(thread.parentMessage) + val replyToText = "$prefix$parentMessageText" + binding.replyToTextView.text = replyToText + } + + private fun bindUnreadCountBadge(currentUser: User?) { + val unreadCount = thread.read + .find { it.user.id == currentUser?.id } + ?.unreadMessages + ?: 0 + if (unreadCount > 0) { + binding.unreadCountBadge.text = + if (unreadCount > MAX_UNREAD_COUNT) "$MAX_UNREAD_COUNT+" else unreadCount.toString() + binding.unreadCountBadge.isVisible = true + } else { + binding.unreadCountBadge.isVisible = false + } + } + + private fun bindLatestReply() { + val latestReply = thread.latestReplies.lastOrNull() + if (latestReply != null) { + // User avatar + binding.latestReplyMessageView.binding.userAvatarView.setUser(latestReply.user) + // Sender name + binding.latestReplyMessageView.binding.senderNameLabel.text = latestReply.user.name.bold() + // Reply text + binding.latestReplyMessageView.binding.messageLabel.text = formatMessage(latestReply) + // Timestamp + binding.latestReplyMessageView.binding.messageTimeLabel.text = + ChatUI.dateFormatter.formatDate(latestReply.createdAt ?: latestReply.createdLocallyAt) + binding.latestReplyMessageView.isVisible = true + } else { + // Note: should never happen + binding.latestReplyMessageView.isVisible = false + } + } + + private fun formatMessage(message: Message): CharSequence { + val attachmentsText = message.getAttachmentsText() + val displayedText = message.getTranslatedText() + val previewText = displayedText.trim() + return listOf(previewText, attachmentsText) + .filterNot { it.isNullOrEmpty() } + .joinTo(SpannableStringBuilder(), " ") + } + + private companion object { + private const val MAX_UNREAD_COUNT = 99 + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadListLoadingMoreViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadListLoadingMoreViewHolder.kt new file mode 100644 index 00000000000..b1492d0f09c --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/threads/list/adapter/viewholder/internal/ThreadListLoadingMoreViewHolder.kt @@ -0,0 +1,35 @@ +/* + * 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.android.ui.feature.threads.list.adapter.viewholder.internal + +import android.view.ViewGroup +import io.getstream.chat.android.ui.databinding.StreamUiItemThreadListLoadingMoreBinding +import io.getstream.chat.android.ui.feature.threads.list.adapter.ThreadListItem +import io.getstream.chat.android.ui.feature.threads.list.adapter.viewholder.BaseThreadListItemViewHolder +import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater + +/** + * ViewHolder for the thread list loading more indicator item. + */ +internal class ThreadListLoadingMoreViewHolder( + parentView: ViewGroup, +) : BaseThreadListItemViewHolder( + itemView = StreamUiItemThreadListLoadingMoreBinding.inflate(parentView.streamThemeInflater, parentView, false).root, +) { + + override fun bind(item: ThreadListItem.LoadingMoreItem) = Unit // No-op +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/TransformStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/TransformStyle.kt index cf384d10607..f6f3153b784 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/TransformStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/TransformStyle.kt @@ -44,6 +44,7 @@ import io.getstream.chat.android.ui.feature.messages.list.reactions.view.ViewRea import io.getstream.chat.android.ui.feature.pinned.list.PinnedMessageListViewStyle import io.getstream.chat.android.ui.feature.search.SearchInputViewStyle import io.getstream.chat.android.ui.feature.search.list.SearchResultListViewStyle +import io.getstream.chat.android.ui.feature.threads.list.ThreadListViewStyle import io.getstream.chat.android.ui.widgets.avatar.AvatarStyle import io.getstream.chat.android.ui.widgets.typing.TypingIndicatorViewStyle @@ -142,6 +143,9 @@ public object TransformStyle { @JvmStatic public var audioRecordPlayerViewStyle: StyleTransformer = noopTransformer() + @JvmStatic + public var threadListViewStyle: StyleTransformer = noopTransformer() + private fun noopTransformer() = StyleTransformer { it } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt index 51f134e7ecd..01fde74ff20 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt @@ -392,11 +392,11 @@ public class MessageListViewModel( */ private fun onBackButtonPressed() { mode.value?.run { - when (this) { - is MessageMode.Normal -> { + when { + this is MessageMode.Normal || messageListController.isStartedForThread -> { stateMerger.postValue(State.NavigateUp) } - is MessageMode.MessageThread -> { + this is MessageMode.MessageThread -> { onNormalModeEntered() } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModel.kt new file mode 100644 index 00000000000..345ea0a0d1c --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModel.kt @@ -0,0 +1,54 @@ +/* + * 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.android.ui.viewmodel.threads + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import io.getstream.chat.android.ui.common.feature.threads.ThreadListController +import io.getstream.chat.android.ui.common.state.threads.ThreadListState + +/** + * ViewModel responsible for managing the state of a threads list. + * + * @param controller The [ThreadListController] handling the business logic and the state management for the threads + * list. + */ +public class ThreadListViewModel(private val controller: ThreadListController) : ViewModel() { + + /** + * The current thread list state. + */ + public val state: LiveData = controller.state.asLiveData() + + /** + * Loads the initial data when requested. + * Overrides all previously retrieved data. + */ + public fun load() { + controller.load() + } + + /** + * Loads more data when requested. + * + * Does nothing if the end of the list has already been reached or loading is already in progress. + */ + public fun loadNextPage() { + controller.loadNextPage() + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModelBinding.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModelBinding.kt new file mode 100644 index 00000000000..b55d3b8a6be --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadListViewModelBinding.kt @@ -0,0 +1,42 @@ +/* + * 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. + */ + +@file:JvmName("ThreadListViewModelBinding") + +package io.getstream.chat.android.ui.viewmodel.threads + +import androidx.lifecycle.LifecycleOwner +import io.getstream.chat.android.ui.feature.threads.list.ThreadListView + +/** + * Binds [ThreadListView] to a [ThreadListViewModel], updating the view's state based on + * data provided by the ViewModel and propagating view events to the ViewModel as needed. + * + * This function sets listeners on the view and ViewModel. Call this method + * before setting any additional listeners on these objects yourself. + */ +@JvmName("bind") +public fun ThreadListViewModel.bindView(view: ThreadListView, lifecycleOwner: LifecycleOwner) { + state.observe(lifecycleOwner) { state -> + when { + state.threads.isEmpty() && state.isLoading -> view.showLoading() + else -> view.showThreads(state.threads, state.isLoadingMore) + } + view.showUnreadThreadsBanner(state.unseenThreadsCount) + } + view.setUnreadThreadsBannerClickListener(::load) + view.setLoadMoreListener(::loadNextPage) +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadsViewModelFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadsViewModelFactory.kt new file mode 100644 index 00000000000..8b0f5cc8e15 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/threads/ThreadsViewModelFactory.kt @@ -0,0 +1,51 @@ +/* + * 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.android.ui.viewmodel.threads + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.getstream.chat.android.ui.common.feature.threads.ThreadListController + +/** + * A ViewModel factory for creating a [ThreadListViewModel]. + * + * @see ThreadListViewModel + * + * @param threadLimit The number of threads to load per page. + * @param threadReplyLimit The number of replies per thread to load. + * @param threadParticipantLimit The number of participants per thread to load. + */ +public class ThreadsViewModelFactory( + private val threadLimit: Int = ThreadListController.DEFAULT_THREAD_LIMIT, + private val threadReplyLimit: Int = ThreadListController.DEFAULT_THREAD_REPLY_LIMIT, + private val threadParticipantLimit: Int = ThreadListController.DEFAULT_THREAD_PARTICIPANT_LIMIT, +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + require(modelClass == ThreadListViewModel::class.java) { + "ThreadsViewModelFactory can only create instances of ThreadListViewModel" + } + @Suppress("UNCHECKED_CAST") + return ThreadListViewModel( + controller = ThreadListController( + threadLimit = threadLimit, + threadReplyLimit = threadReplyLimit, + threadParticipantLimit = threadParticipantLimit, + ), + ) as T + } +} diff --git a/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_thread.xml b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_thread.xml new file mode 100644 index 00000000000..2a2936a530c --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_thread.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_threads_empty.xml b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_threads_empty.xml new file mode 100644 index 00000000000..602bdbf230e --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_threads_empty.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_union.xml b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_union.xml new file mode 100644 index 00000000000..0826e2b4597 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_ic_union.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_unread_threads_banner.xml b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_unread_threads_banner.xml new file mode 100644 index 00000000000..5973a46f13f --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/drawable/stream_ui_shape_unread_threads_banner.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list.xml new file mode 100644 index 00000000000..8568f280ead --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list_loading_more.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list_loading_more.xml new file mode 100644 index 00000000000..ed811e5a58a --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_thread_list_loading_more.xml @@ -0,0 +1,39 @@ + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_thread_list_view.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_thread_list_view.xml new file mode 100644 index 00000000000..5cf301eccea --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_thread_list_view.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/values/attrs.xml b/stream-chat-android-ui-components/src/main/res/values/attrs.xml index 460493cba7e..84b41285881 100644 --- a/stream-chat-android-ui-components/src/main/res/values/attrs.xml +++ b/stream-chat-android-ui-components/src/main/res/values/attrs.xml @@ -57,5 +57,6 @@ + diff --git a/stream-chat-android-ui-components/src/main/res/values/attrs_thread_list_view.xml b/stream-chat-android-ui-components/src/main/res/values/attrs_thread_list_view.xml new file mode 100644 index 00000000000..e3a2256ab2c --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/values/attrs_thread_list_view.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/values/strings_thread_list.xml b/stream-chat-android-ui-components/src/main/res/values/strings_thread_list.xml new file mode 100644 index 00000000000..3976600e83a --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/values/strings_thread_list.xml @@ -0,0 +1,24 @@ + + + + No threads here yet… + replied to:  + + %d new thread + %d new threads + + \ No newline at end of file diff --git a/stream-chat-android-ui-components/src/main/res/values/styles.xml b/stream-chat-android-ui-components/src/main/res/values/styles.xml index 56cb341e76b..720f01271eb 100644 --- a/stream-chat-android-ui-components/src/main/res/values/styles.xml +++ b/stream-chat-android-ui-components/src/main/res/values/styles.xml @@ -224,40 +224,80 @@ @@ -273,7 +313,9 @@ true topRight 0dp - ?attr/streamUiGlobalAvatarOnlineIndicatorColor + + ?attr/streamUiGlobalAvatarOnlineIndicatorColor + @color/stream_ui_white circle @@ -290,7 +332,9 @@ 6dp @drawable/stream_ui_ic_down 3dp - @dimen/stream_ui_scroll_button_unread_badge_text_size + + @dimen/stream_ui_scroll_button_unread_badge_text_size + @color/stream_ui_literal_white normal @@ -317,7 +361,8 @@ @color/stream_ui_text_color_primary normal @dimen/stream_ui_text_small - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + normal @dimen/stream_ui_text_small @color/stream_ui_text_color_secondary @@ -355,17 +400,20 @@ @dimen/stream_ui_text_small - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + normal @dimen/stream_ui_text_small - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + normal @color/stream_ui_bars_background - @color/stream_ui_grey_gainsboro + @color/stream_ui_grey_gainsboro + @dimen/stream_ui_text_medium @@ -381,7 +429,8 @@ @color/stream_ui_accent_blue @color/stream_ui_accent_blue @color/stream_ui_blue_alice - @color/stream_ui_blue_alice + @color/stream_ui_blue_alice + @color/stream_ui_literal_transparent @@ -398,8 +447,10 @@ @color/stream_ui_grey_whisper - @color/stream_ui_grey_gainsboro - @color/stream_ui_grey_whisper + @color/stream_ui_grey_gainsboro + + @color/stream_ui_grey_whisper + @color/stream_ui_white @@ -416,7 +467,8 @@ @color/stream_ui_grey_whisper @dimen/stream_ui_text_medium - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + italic @@ -490,7 +542,8 @@ normal @dimen/stream_ui_text_medium - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + normal @dimen/stream_ui_text_medium @@ -506,14 +559,17 @@ bottom - @color/stream_ui_text_color_secondary + @color/stream_ui_text_color_secondary + @dimen/stream_ui_text_small normal @drawable/stream_ui_ic_pin @color/stream_ui_highlight - @dimen/stream_ui_message_viewholder_avatar_missing_margin + + @dimen/stream_ui_message_viewholder_avatar_missing_margin + 0dp @@ -521,14 +577,24 @@ 75% - @style/StreamUi.AudioRecordPlayerView - @style/StreamUi.AudioRecordPlayerView + + @style/StreamUi.AudioRecordPlayerView + + + @style/StreamUi.AudioRecordPlayerView + @@ -570,20 +640,29 @@ @@ -607,7 +687,9 @@ @dimen/stream_ui_text_medium - @color/stream_ui_text_color_primary + + @color/stream_ui_text_color_primary + normal @@ -640,8 +722,11 @@ @color/stream_ui_accent_blue true @drawable/stream_ui_ic_pen - @color/stream_ui_icon_button_background_selector - @drawable/stream_ui_divider + @color/stream_ui_icon_button_background_selector + + + @drawable/stream_ui_divider + @@ -677,11 +763,18 @@ @color/stream_ui_white_smoke - @dimen/stream_ui_channel_list_item_margin_start + @dimen/stream_ui_channel_list_item_margin_start + @dimen/stream_ui_channel_list_item_margin_end - @dimen/stream_ui_channel_list_item_title_margin_start - @dimen/stream_ui_channel_list_item_vertical_spacer_height - @dimen/stream_ui_channel_list_item_vertical_spacer_position + + @dimen/stream_ui_channel_list_item_title_margin_start + + + @dimen/stream_ui_channel_list_item_vertical_spacer_height + + + @dimen/stream_ui_channel_list_item_vertical_spacer_position + @dimen/stream_ui_channel_item_title @color/stream_ui_text_color_primary bold @@ -690,7 +783,8 @@ @color/stream_ui_text_color_secondary normal - @dimen/stream_ui_channel_item_message_date + @dimen/stream_ui_channel_item_message_date + @color/stream_ui_text_color_secondary normal @@ -788,12 +882,16 @@ @@ -916,29 +1087,41 @@ @@ -1063,83 +1263,177 @@ + +