diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f76b7e3181..c79bc595623 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,8 @@
### ⬆️ Improved
### ✅ Added
+- Added the parameter `messageId: String?` to `MessageListViewModel` and `MessageListViewModelFactory`. If `navigateToThreadViaNotification` is set to true (see the changelog entry below), it will enable navigating to threads upon clicking a push notification triggered by a thread message. [#4612](https://github.com/GetStream/stream-chat-android/pull/4612)
+- Added the feature flag boolean `navigateToThreadViaNotification: Boolean` to `MessageListViewModel` and `MessageListViewModelFactory`. If it is set to true and a thread message has been received via push notification, clicking on the notification will make the SDK automatically navigate to the thread. If set to false, the SDK will always navigate to the channel containing the thread without navigating to the thread itself. [#4612](https://github.com/GetStream/stream-chat-android/pull/4612)
### ⚠️ Changed
diff --git a/stream-chat-android-compose-sample/detekt-baseline.xml b/stream-chat-android-compose-sample/detekt-baseline.xml
index 91f711c92ba..e202aa75943 100644
--- a/stream-chat-android-compose-sample/detekt-baseline.xml
+++ b/stream-chat-android-compose-sample/detekt-baseline.xml
@@ -3,7 +3,7 @@
LongMethod:CustomLoginActivity.kt$CustomLoginActivity$@Composable fun CustomLoginScreen( onBackButtonClick: () -> Unit, onLoginButtonClick: (UserCredentials) -> Unit, )
- LongMethod:MessagesActivity.kt$MessagesActivity$@OptIn(ExperimentalFoundationApi::class) @Composable fun MyCustomUi()
+ LongMethod:MessagesActivity.kt$MessagesActivity$@Composable fun MyCustomUi()
MagicNumber:ChannelsActivity.kt$ChannelsActivity$0.5f
MagicNumber:MessagesActivity.kt$MessagesActivity$7f
MaxLineLength:MessagesActivity.kt$MessagesActivity$lazyListState = if (listViewModel.currentMessagesState.parentMessageId != null) rememberLazyListState() else lazyListState
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt
index c648de47c8e..7f2ac396007 100644
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt
@@ -48,10 +48,11 @@ object ChatHelper {
)
val notificationHandler = NotificationHandlerFactory.createNotificationHandler(
context = context,
- newMessageIntent = { _: String, channelType: String, channelId: String ->
+ newMessageIntent = { messageId: String, channelType: String, channelId: String ->
StartupActivity.createIntent(
context = context,
- channelId = "$channelType:$channelId"
+ channelId = "$channelType:$channelId",
+ messageId = messageId
)
}
)
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/ChannelsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/ChannelsActivity.kt
index 161933264f9..ad8adf23a9c 100644
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/ChannelsActivity.kt
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/ChannelsActivity.kt
@@ -230,7 +230,13 @@ class ChannelsActivity : BaseConnectedActivity() {
}
private fun openMessages(channel: Channel) {
- startActivity(MessagesActivity.createIntent(this, channel.cid))
+ startActivity(
+ MessagesActivity.createIntent(
+ context = this,
+ channelId = channel.cid,
+ messageId = null
+ )
+ )
}
private fun openUserLogin() {
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt
index e1f8ab35303..44830fbd648 100644
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt
@@ -21,7 +21,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -80,6 +79,7 @@ class MessagesActivity : BaseConnectedActivity() {
context = this,
channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: "",
deletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE,
+ messageId = intent.getStringExtra(KEY_MESSAGE_ID)
)
}
@@ -91,13 +91,16 @@ class MessagesActivity : BaseConnectedActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: return
+ val messageId = intent.getStringExtra(KEY_MESSAGE_ID)
setContent {
ChatTheme(dateFormatter = ChatApp.dateFormatter) {
MessagesScreen(
channelId = channelId,
onBackPressed = { finish() },
- onHeaderActionClick = {}
+ onHeaderActionClick = {},
+ messageId = messageId,
+ navigateToThreadViaNotification = true
)
// MyCustomUi()
@@ -105,7 +108,6 @@ class MessagesActivity : BaseConnectedActivity() {
}
}
- @OptIn(ExperimentalFoundationApi::class)
@Composable
fun MyCustomUi() {
val isShowingAttachments = attachmentsPickerViewModel.isShowingAttachments
@@ -299,10 +301,18 @@ class MessagesActivity : BaseConnectedActivity() {
companion object {
private const val KEY_CHANNEL_ID = "channelId"
+ private const val KEY_MESSAGE_ID = "messageId"
- fun createIntent(context: Context, channelId: String): Intent {
+ fun createIntent(
+ context: Context,
+ channelId: String,
+ messageId: String?,
+ ): Intent {
return Intent(context, MessagesActivity::class.java).apply {
putExtra(KEY_CHANNEL_ID, channelId)
+ if (messageId != null) {
+ putExtra(KEY_MESSAGE_ID, messageId)
+ }
}
}
}
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt
index feb5a78a7a4..dd6bbcfa95d 100644
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/StartupActivity.kt
@@ -46,9 +46,17 @@ class StartupActivity : AppCompatActivity() {
if (intent.hasExtra(KEY_CHANNEL_ID)) {
// Navigating from push, route to the messages screen
val channelId = requireNotNull(intent.getStringExtra(KEY_CHANNEL_ID))
+ val messageId = intent.getStringExtra(KEY_MESSAGE_ID)
+
TaskStackBuilder.create(this)
.addNextIntent(ChannelsActivity.createIntent(this))
- .addNextIntent(MessagesActivity.createIntent(this, channelId))
+ .addNextIntent(
+ MessagesActivity.createIntent(
+ context = this,
+ channelId = channelId,
+ messageId = messageId
+ )
+ )
.startActivities()
} else {
// Logged in, navigate to the channels screen
@@ -63,10 +71,18 @@ class StartupActivity : AppCompatActivity() {
companion object {
private const val KEY_CHANNEL_ID = "channelId"
+ private const val KEY_MESSAGE_ID = "messageId"
- fun createIntent(context: Context, channelId: String): Intent {
+ fun createIntent(
+ context: Context,
+ channelId: String,
+ messageId: String?,
+ ): Intent {
return Intent(context, StartupActivity::class.java).apply {
putExtra(KEY_CHANNEL_ID, channelId)
+ if (messageId != null) {
+ putExtra(KEY_MESSAGE_ID, messageId)
+ }
}
}
}
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 e4c92d313a6..78f86e02b0a 100644
--- a/stream-chat-android-compose/api/stream-chat-android-compose.api
+++ b/stream-chat-android-compose/api/stream-chat-android-compose.api
@@ -1300,7 +1300,7 @@ public final class io/getstream/chat/android/compose/ui/components/userreactions
}
public final class io/getstream/chat/android/compose/ui/messages/MessagesScreenKt {
- public static final fun MessagesScreen (Ljava/lang/String;IZZZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
+ public static final fun MessagesScreen (Ljava/lang/String;IZZZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZLandroidx/compose/runtime/Composer;III)V
}
public final class io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPickerKt {
@@ -1960,8 +1960,8 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageC
public final class io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel : androidx/lifecycle/ViewModel {
public static final field $stable I
- public fun (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Lio/getstream/chat/android/compose/handlers/ClipboardHandler;IZZZJLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;)V
- public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Lio/getstream/chat/android/compose/handlers/ClipboardHandler;IZZZJLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Lio/getstream/chat/android/compose/handlers/ClipboardHandler;IZZZJLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;Ljava/lang/String;Z)V
+ public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Lio/getstream/chat/android/compose/handlers/ClipboardHandler;IZZZJLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V
public static synthetic fun banUser$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V
public final fun clearNewMessageState ()V
@@ -2007,8 +2007,8 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL
public final class io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory {
public static final field $stable I
- public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;ZIIJZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;J)V
- public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;ZIIJZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;ZIIJZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;JLjava/lang/String;Z)V
+ public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;ZIIJZZLio/getstream/chat/android/common/state/DeletedMessageVisibility;Lio/getstream/chat/android/common/state/MessageFooterVisibility;JLjava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel;
}
diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml
index 5f1003a838f..c58e82b285d 100644
--- a/stream-chat-android-compose/detekt-baseline.xml
+++ b/stream-chat-android-compose/detekt-baseline.xml
@@ -21,7 +21,6 @@
LongParameterList:MessageContainer.kt$( messageItem: MessageItemState, onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, onThreadClick: (Message) -> Unit, onGiphyActionClick: (GiphyAction) -> Unit, onQuotedMessageClick: (Message) -> Unit, onImagePreviewResult: (ImagePreviewResult?) -> Unit, )
LongParameterList:MessageList.kt$( messageListItem: MessageListItemState, onImagePreviewResult: (ImagePreviewResult?) -> Unit, onThreadClick: (Message) -> Unit, onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit, onQuotedMessageClick: (Message) -> Unit, )
LongParameterList:Messages.kt$( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, )
- LongParameterList:MessagesScreen.kt$( context: Context, channelId: String, enforceUniqueReactions: Boolean, messageLimit: Int, showDateSeparators: Boolean, showSystemMessages: Boolean, deletedMessageVisibility: DeletedMessageVisibility, messageFooterVisibility: MessageFooterVisibility, )
MagicNumber:AvatarPosition.kt$3
MagicNumber:FileAttachmentContent.kt$0.85f
MagicNumber:FilesPicker.kt$6f
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 70fbdb6bd12..467ab474604 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
@@ -27,7 +27,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -108,6 +107,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
* @param onBackPressed Handler for when the user taps on the Back button and/or the system
* back button.
* @param onHeaderActionClick Handler for when the user taps on the header action.
+ * @param messageId The ID of the message which we wish to focus on, if such exists.
+ * @param navigateToThreadViaNotification If true, when a thread message arrives in a push notification,
+ * clicking it will automatically open the thread in which the message is located. If false, the SDK will always
+ * navigate to the channel containing the thread but will not navigate to the thread itself.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalCoroutinesApi::class)
@@ -123,6 +126,8 @@ public fun MessagesScreen(
messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.WithTimeDifference(),
onBackPressed: () -> Unit = {},
onHeaderActionClick: (channel: Channel) -> Unit = {},
+ messageId: String? = null,
+ navigateToThreadViaNotification: Boolean = false,
) {
val factory = buildViewModelFactory(
context = LocalContext.current,
@@ -132,7 +137,9 @@ public fun MessagesScreen(
showSystemMessages = showSystemMessages,
showDateSeparators = showDateSeparators,
deletedMessageVisibility = deletedMessageVisibility,
- messageFooterVisibility = messageFooterVisibility
+ messageFooterVisibility = messageFooterVisibility,
+ messageId = messageId,
+ navigateToThreadViaNotification = navigateToThreadViaNotification,
)
val listViewModel = viewModel(MessageListViewModel::class.java, factory = factory)
@@ -140,6 +147,12 @@ public fun MessagesScreen(
val attachmentsPickerViewModel =
viewModel(AttachmentsPickerViewModel::class.java, factory = factory)
+ val messageMode = listViewModel.messageMode
+
+ if (messageMode is MessageMode.MessageThread) {
+ composerViewModel.setMessageMode(messageMode)
+ }
+
val backAction = {
val isInThread = listViewModel.isInThread
val isShowingOverlay = listViewModel.isShowingOverlay
@@ -163,7 +176,6 @@ public fun MessagesScreen(
modifier = Modifier.fillMaxSize(),
topBar = {
if (showHeader) {
- val messageMode = listViewModel.messageMode
val connectionState by listViewModel.connectionState.collectAsState()
val user by listViewModel.user.collectAsState()
@@ -393,7 +405,7 @@ private fun BoxScope.MessagesScreenMenus(
* @param selectedMessageState The state of the currently selected message.
* @param selectedMessage The currently selected message.
*/
-@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun BoxScope.MessagesScreenReactionsPicker(
listViewModel: MessageListViewModel,
@@ -569,8 +581,13 @@ private fun MessageDialogs(listViewModel: MessageListViewModel) {
* @param showSystemMessages If we should show system messages or not. * @param deletedMessageVisibility The behavior of deleted messages in the list.
* @param deletedMessageVisibility The behavior of deleted messages in the list and if they're visible or not.
* @param messageFooterVisibility The behavior of message footers in the list and their visibility.
+ * @param messageId The ID of the message which we wish to focus on, if such exists.
+ * @param navigateToThreadViaNotification If true, when a thread message arrives in a push notification,
+ * clicking it will automatically open the thread in which the message is located. If false, the SDK will always
+ * navigate to the channel containing the thread but will not navigate to the thread itself.
*/
@ExperimentalCoroutinesApi
+@Suppress("LongParameterList")
private fun buildViewModelFactory(
context: Context,
channelId: String,
@@ -580,6 +597,8 @@ private fun buildViewModelFactory(
showSystemMessages: Boolean,
deletedMessageVisibility: DeletedMessageVisibility,
messageFooterVisibility: MessageFooterVisibility,
+ messageId: String? = null,
+ navigateToThreadViaNotification: Boolean = false,
): MessagesViewModelFactory {
return MessagesViewModelFactory(
context = context,
@@ -589,6 +608,8 @@ private fun buildViewModelFactory(
showDateSeparators = showDateSeparators,
showSystemMessages = showSystemMessages,
deletedMessageVisibility = deletedMessageVisibility,
- messageFooterVisibility = messageFooterVisibility
+ messageFooterVisibility = messageFooterVisibility,
+ messageId = messageId,
+ navigateToThreadViaNotification = navigateToThreadViaNotification,
)
}
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 c1f9307bdd3..fbe52c00ff1 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
@@ -74,7 +74,9 @@ import io.getstream.chat.android.compose.ui.util.isError
import io.getstream.chat.android.compose.ui.util.isSystem
import io.getstream.chat.android.core.internal.exhaustive
import io.getstream.chat.android.offline.extensions.cancelEphemeralMessage
+import io.getstream.chat.android.offline.extensions.getMessageUsingCache
import io.getstream.chat.android.offline.extensions.getRepliesAsState
+import io.getstream.chat.android.offline.extensions.getRepliesAsStateCall
import io.getstream.chat.android.offline.extensions.globalState
import io.getstream.chat.android.offline.extensions.loadMessageById
import io.getstream.chat.android.offline.extensions.loadOlderMessages
@@ -83,6 +85,7 @@ import io.getstream.chat.android.offline.plugin.state.channel.ChannelState
import io.getstream.chat.android.offline.plugin.state.channel.thread.ThreadState
import io.getstream.logging.StreamLog
import io.getstream.logging.TaggedLogger
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -111,6 +114,10 @@ import java.util.concurrent.TimeUnit
* @param showSystemMessages Enables or disables system messages in the list.
* @param dateSeparatorThresholdMillis The threshold in millis used to generate date separator items, if enabled.
* @param deletedMessageVisibility The behavior of deleted messages in the list and if they're visible or not.
+ * @param messageId The ID of the message which we wish to focus on, if such exists.
+ * @param navigateToThreadViaNotification If true, when a thread message arrives in a push notification,
+ * clicking it will automatically open the thread in which the message is located. If false, the SDK will always
+ * navigate to the channel containing the thread but will not navigate to the thread itself.
*/
@Suppress("TooManyFunctions", "LargeClass", "TooManyFunctions")
public class MessageListViewModel(
@@ -124,6 +131,8 @@ public class MessageListViewModel(
private val dateSeparatorThresholdMillis: Long = TimeUnit.HOURS.toMillis(DateSeparatorDefaultHourThreshold),
private val deletedMessageVisibility: DeletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE,
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.WithTimeDifference(),
+ private val messageId: String? = null,
+ private val navigateToThreadViaNotification: Boolean = false,
) : ViewModel() {
/**
@@ -142,6 +151,7 @@ public class MessageListViewModel(
* e.g. send messages, delete messages, etc...
* For a full list @see [io.getstream.chat.android.client.models.ChannelCapabilities].
*/
+ @OptIn(ExperimentalCoroutinesApi::class)
private val ownCapabilities: StateFlow> =
channelState.filterNotNull()
.flatMapLatest { it.channelData }
@@ -269,6 +279,10 @@ public class MessageListViewModel(
observeTypingUsers()
observeMessages()
observeChannel()
+
+ if (messageId != null) {
+ onOpenedFromPushNotification(messageId)
+ }
}
/**
@@ -342,11 +356,13 @@ public class MessageListViewModel(
/**
* Starts observing the list of typing users.
*/
+ @OptIn(ExperimentalCoroutinesApi::class)
private fun observeTypingUsers() {
viewModelScope.launch {
- channelState.filterNotNull().flatMapLatest { it.typing }.collect {
- typingUsers = it.users
- }
+ channelState.filterNotNull()
+ .flatMapLatest { it.typing }.collect {
+ typingUsers = it.users
+ }
}
}
@@ -354,23 +370,25 @@ public class MessageListViewModel(
* Starts observing the current [Channel] created from [ChannelState]. It emits new data when either
* channel data, member count or online member count updates.
*/
+ @OptIn(ExperimentalCoroutinesApi::class)
private fun observeChannel() {
viewModelScope.launch {
- channelState.filterNotNull().flatMapLatest { state ->
- combine(
- state.channelData,
- state.membersCount,
- state.watcherCount,
- ) { _, _, _ ->
- state.toChannel()
+ channelState.filterNotNull()
+ .flatMapLatest { state ->
+ combine(
+ state.channelData,
+ state.membersCount,
+ state.watcherCount,
+ ) { _, _, _ ->
+ state.toChannel()
+ }
+ }.collect { channel ->
+ chatClient.notifications.dismissChannelNotifications(
+ channelType = channel.type,
+ channelId = channel.id
+ )
+ setCurrentChannel(channel)
}
- }.collect { channel ->
- chatClient.notifications.dismissChannelNotifications(
- channelType = channel.type,
- channelId = channel.id
- )
- setCurrentChannel(channel)
- }
}
}
@@ -720,6 +738,40 @@ public class MessageListViewModel(
)
}
+ /**
+ * Changes the current [messageMode] to be [Thread] with [ThreadState] and Loads thread data using ChatClient
+ * directly. The data is observed by using [ThreadState].
+ *
+ * The difference between [loadThread] and [loadThreadSequential] is that the latter makes a call to a [ChatClient]
+ * extension function which will return a [ThreadState] instance only once the API call had finished, while the
+ * former calls a different function which returns a [ThreadState] instance immediately after the API request has
+ * fired, regardless of its completion state.
+ *
+ * @param parentMessage The message with the thread we want to observe.
+ */
+ private suspend fun loadThreadSequential(parentMessage: Message) {
+ val result = chatClient.getRepliesAsStateCall(parentMessage.id, DefaultMessageLimit).await()
+ val channelState = channelState.value ?: return
+
+ if (result.isSuccess) {
+ val threadState = result.data()
+
+ messageMode = MessageMode.MessageThread(parentMessage, threadState)
+ observeThreadMessages(
+ threadId = threadState.parentId,
+ messages = threadState.messages,
+ endOfOlderMessages = threadState.endOfOlderMessages,
+ reads = channelState.reads
+ )
+ } else {
+ val error = result.error()
+
+ logger.e {
+ "[loadThread] -> Could not load thread: ${error.message}. Cause: ${error.cause?.message}"
+ }
+ }
+ }
+
/**
* Observes the currently active thread. In process, this
* creates a [threadJob] that we can cancel once we leave the thread.
@@ -1198,6 +1250,54 @@ public class MessageListViewModel(
}.exhaustive.enqueue()
}
+ /**
+ * Fetches the message with the according message ID. If the given message is in a thread it will set the
+ * mode to thread mode, otherwise the ViewModel will stay in normal mode.
+ *
+ * @param messageId The ID of the message received via push notification.
+ */
+ private fun onOpenedFromPushNotification(messageId: String) {
+ viewModelScope.launch {
+ val result = chatClient.getMessageUsingCache(messageId = messageId).await()
+ val parentMessageId = result.data().parentId
+
+ // The channel will be automatically loaded given so we only need to
+ // account for opening threads when thread messages arrive via PNs
+ if (result.isSuccess && parentMessageId != null && navigateToThreadViaNotification) {
+ openThreadFromPushNotification(parentMessageId)
+ } else if (result.isError) {
+ val error = result.error()
+
+ logger.e {
+ "[onOpenedFromPushNotification] -> Could not load message: ${error.message}. " +
+ "Cause: ${error.cause?.message}"
+ }
+ }
+ }
+ }
+
+ /**
+ * Opens the thread the belonging to the parent message given by [parentMessageId].
+ *
+ * @param parentMessageId The ID of the parent message hosting the thread message received via push notification.
+ */
+ private fun openThreadFromPushNotification(parentMessageId: String) {
+ viewModelScope.launch {
+ val result = chatClient.getMessageUsingCache(parentMessageId).await()
+
+ if (result.isSuccess) {
+ loadThreadSequential(result.data())
+ } else {
+ val error = result.error()
+
+ logger.e {
+ "[openThreadFromPushNotification] -> Could not load thread parent message: ${error.message}. " +
+ "Cause: ${error.cause?.message}"
+ }
+ }
+ }
+ }
+
/**
* Scrolls to message if in list otherwise get the message from backend.
*
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt
index f95c666199f..332e8de1181 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt
@@ -46,6 +46,10 @@ import java.util.concurrent.TimeUnit
* @param deletedMessageVisibility The behavior of deleted messages in the list and if they're visible or not.
* @param messageFooterVisibility The behavior of message footers in the list and their visibility.
* @param dateSeparatorThresholdMillis The millisecond amount that represents the threshold for adding date separators.
+ * @param messageId The ID of the message which we wish to focus on, if such exists.
+ * @param navigateToThreadViaNotification If true, when a thread message arrives in a push notification,
+ * clicking it will automatically open the thread in which the message is located. If false, the SDK will always
+ * navigate to the channel containing the thread but will not navigate to the thread itself.
*/
public class MessagesViewModelFactory(
private val context: Context,
@@ -60,6 +64,8 @@ public class MessagesViewModelFactory(
private val deletedMessageVisibility: DeletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE,
private val messageFooterVisibility: MessageFooterVisibility = MessageFooterVisibility.WithTimeDifference(),
private val dateSeparatorThresholdMillis: Long = TimeUnit.HOURS.toMillis(MessageListViewModel.DateSeparatorDefaultHourThreshold),
+ private val messageId: String? = null,
+ private val navigateToThreadViaNotification: Boolean = false,
) : ViewModelProvider.Factory {
/**
@@ -88,7 +94,9 @@ public class MessagesViewModelFactory(
showSystemMessages = showSystemMessages,
deletedMessageVisibility = deletedMessageVisibility,
messageFooterVisibility = messageFooterVisibility,
- dateSeparatorThresholdMillis = dateSeparatorThresholdMillis
+ dateSeparatorThresholdMillis = dateSeparatorThresholdMillis,
+ messageId = messageId,
+ navigateToThreadViaNotification = navigateToThreadViaNotification
)
},
AttachmentsPickerViewModel::class.java to {