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 {