Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[AND-3] Threads V2 - XML #5491

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c324f6f
[AND-3] Implement ThreadListView for XML SDK.
Nov 25, 2024
7ec1ca0
Merge branch 'develop' into feature/threads_v2_xml
Nov 26, 2024
50b1249
Remove thread list dividers.
Nov 26, 2024
10ba329
[AND-3] Add Threads tab to sample app.
Nov 26, 2024
1f11594
Add ThreadListView to CHANGELOG.md.
Nov 27, 2024
94c346d
Fix sample app thread navigation.
Nov 27, 2024
9e84534
Merge branch 'develop' into feature/threads_v2_xml
Nov 27, 2024
5f34bf8
[AND-3] Create ThreadListItemViewHolderFactory for ViewHolder customi…
Nov 28, 2024
b7298e0
Merge branch 'develop' into feature/threads_v2_xml
Nov 29, 2024
fd76ab8
Merge branch 'develop' into feature/threads_v2_xml
Dec 2, 2024
d02ebe2
[AND-3] Add @JvmName to ThreadListViewModelBinding.kt
Dec 2, 2024
a163915
Merge branch 'develop' into feature/threads_v2_xml
Dec 3, 2024
1feeb69
[AND-3] Implement ThreadListView for XML SDK.
Nov 25, 2024
39e48ff
Remove thread list dividers.
Nov 26, 2024
a1b0d0b
[AND-3] Add Threads tab to sample app.
Nov 26, 2024
7425183
Add ThreadListView to CHANGELOG.md.
Nov 27, 2024
058dc3d
Fix sample app thread navigation.
Nov 27, 2024
6165cda
[AND-3] Create ThreadListItemViewHolderFactory for ViewHolder customi…
Nov 28, 2024
986dad2
[AND-3] Add @JvmName to ThreadListViewModelBinding.kt
Dec 2, 2024
3e78ada
Merge remote-tracking branch 'origin/feature/threads_v2_xml' into fea…
Dec 5, 2024
7ebe487
[AND-3] Fix crash in ThreadListViewStyle due to wrong 'use' import.
Dec 5, 2024
f00eadf
[AND-15] Fix demo app unread counts.
Dec 6, 2024
4231b53
[AND-15] Fix current user mentions displayed in bold.
Dec 6, 2024
eb9fb6f
Merge branch 'develop' into feature/threads_v2_xml
Dec 9, 2024
5d2f9f7
[AND-15] Update MessageList back navigation when opened for thread.
Dec 9, 2024
702fce4
[AND-15] Fix unread badge always displayed.
Dec 9, 2024
febf31d
Merge branch 'refs/heads/develop' into feature/threads_v2_xml
Dec 9, 2024
bdc361e
[AND-15] Revert mark read/unread fixes.
Dec 9, 2024
2ef9043
[AND-15] Fix thread replies label not showing (add isThreadMode to Me…
Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -171,6 +172,7 @@ public fun AiMessagesScreen(
)

isShowingOverlay -> listViewModel.selectMessage(null)
isStartedForThread -> onBackPressed()
isInThread -> {
listViewModel.leaveThread()
composerViewModel.leaveThread()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3614,6 +3614,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -193,6 +194,7 @@ public fun MessagesScreen(
)

isShowingOverlay -> listViewModel.selectMessage(null)
isStartedForThread -> onBackPressed()
isInThread -> {
listViewModel.leaveThread()
composerViewModel.leaveThread()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ public class MessageListViewModel(
*/
public val messageActions: Set<MessageAction> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
<string name="stream_compose_view_answers">View Comments</string>

<!-- Threads -->
<string name="stream_compose_thread_list_empty_title">No threads here yet...</string>
<string name="stream_compose_thread_list_empty_title">No threads here yet</string>
<plurals name="stream_compose_thread_list_new_threads">
<item quantity="one">%d new thread</item>
<item quantity="other">%d new threads</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,31 +698,43 @@ internal class EventHandlerSequential(

// get the channel, update reads, write the channel
is MessageReadEvent -> {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
// Update corresponding thread if event was received for marking a thread as read
event.thread?.let { threadInfo ->
threadFromPendingUpdateOrRepo(batch, threadInfo.parentMessageId)
?.markAsReadByUser(threadInfo, event.user, event.createdAt)
val thread = event.thread
if (thread != null) {
// Update corresponding thread if event was received for marking a thread as read
threadFromPendingUpdateOrRepo(batch, thread.parentMessageId)
?.markAsReadByUser(thread, event.user, event.createdAt)
?.let(batch::addThread)
} else {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
}
}

is NotificationMarkReadEvent -> {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
val thread = event.thread
if (thread != null) {
// Update corresponding thread if event was received for marking a thread as read
threadFromPendingUpdateOrRepo(batch, thread.parentMessageId)
?.markAsReadByUser(thread, event.user, event.createdAt)
?.let(batch::addThread)
} else {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
}
}
is NotificationMarkUnreadEvent -> {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
// Update corresponding thread if event was received for marking a thread as unread
event.threadId?.let { threadId ->
val threadId = event.threadId
if (threadId != null) {
// Update corresponding thread if event was received for marking a thread as unread
threadFromPendingUpdateOrRepo(batch, threadId)
?.markAsUnreadByUser(event.user, event.createdAt)
?.let(batch::addThread)
} else {
batch.getCurrentChannel(event.cid)
?.updateReads(event.toChannelUserRead())
?.let(batch::addChannel)
}
}
is GlobalUserBannedEvent -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,24 @@ internal class ChannelLogic(
is NotificationChannelTruncatedEvent -> removeMessagesBefore(event.createdAt)
is TypingStopEvent -> channelStateLogic.setTyping(event.user.id, null)
is TypingStartEvent -> channelStateLogic.setTyping(event.user.id, event)
is MessageReadEvent -> channelStateLogic.updateRead(event.toChannelUserRead())
is NotificationMarkReadEvent -> channelStateLogic.updateRead(event.toChannelUserRead())
is NotificationMarkUnreadEvent -> channelStateLogic.updateRead(event.toChannelUserRead())
is MessageReadEvent -> {
// Update reads only if the event is not related to a thread
if (event.thread == null) {
channelStateLogic.updateRead(event.toChannelUserRead())
}
}
is NotificationMarkReadEvent -> {
// Update reads only if the event is not related to a thread
if (event.threadId == null) {
channelStateLogic.updateRead(event.toChannelUserRead())
}
}
is NotificationMarkUnreadEvent -> {
// Update reads only if the event is not related to a thread
if (event.threadId == null) {
channelStateLogic.updateRead(event.toChannelUserRead())
}
}
is NotificationInviteAcceptedEvent -> {
channelStateLogic.addMember(event.member)
channelStateLogic.updateChannelData(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ public class MessageListController(
private val _mode: MutableStateFlow<MessageMode> = MutableStateFlow(MessageMode.Normal)
public val mode: StateFlow<MessageMode> = _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.
*/
Expand Down Expand Up @@ -550,13 +555,17 @@ public class MessageListController(
}
}
.onFirst { channelUserRead ->
val unreadMessages = (channelState.value?.messages?.value ?: emptyList())
.fold(emptyList<Message>()) { acc, message ->
when {
channelUserRead.lastReadMessageId == message.id -> emptyList()
else -> acc + message
val unreadMessages = if (channelUserRead.unreadMessages == 0) {
emptyList()
} else {
(channelState.value?.messages?.value ?: emptyList())
.fold(emptyList<Message>()) { acc, message ->
when {
channelUserRead.lastReadMessageId == message.id -> emptyList()
else -> acc + message
}
}
}
}
unreadLabelState.value = channelUserRead.lastReadMessageId
?.takeUnless { unreadMessages.isEmpty() }
?.takeUnless { unreadMessages.lastOrNull()?.id == it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ class HomeViewModel(
val events: LiveData<Event<UiEvent>> = _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()) }
}
Expand Down Expand Up @@ -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,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
>
<path
android:fillColor="#080707"
android:pathData="M4,4H20V16H5.17L4,17.17V4ZM4,2C2.9,2 2.01,2.9 2.01,4L2,22L6,18H20C21.1,18 22,17.1 22,16V4C22,2.9 21.1,2 20,2H4ZM6,12H14V14H6V12ZM6,9H18V11H6V9ZM6,6H18V8H6V6Z"
/>
</vector>
Loading
Loading