Skip to content

Commit

Permalink
[4608] [v5] Navigate to Thread Messages via PN (#4612)
Browse files Browse the repository at this point in the history
* [4608] Implemented the ability to navigate to thread messages by clicking on push notifications

* [4608] Implemented the ability to navigate to thread messages by clicking on push notifications

* [4608] Implement core and UI side changes allowing more reliable displaying of thread messages when opened by clicking on PNs.

* [4608] Make the load notification worker attempt to fetch the thread parent message if the message that has arrived is a thread message.

* [4608] Appease Detekt

* [4608] Appease Detekt

* [4608] Implement a feature flag for the new `MessageListViewModel` ability to navigate to threads via PNs

* [4608] Implement a feature flag for the new `MessageListViewModel` ability to navigate to threads via PNs

* [4608] Implement a feature flag for the new `MessageListViewModel` ability to navigate to threads via PNs

* [4608] Update the changelog.

* [4608] Switch to using the provided `ChatClient` class parameter

* [4608] Remove leftover logging messages
  • Loading branch information
MarinTolic authored Jan 30, 2023
1 parent 74dac8a commit 950b84c
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
### ✅ Added
- Added a feature flag to `ChatUI` called `showThreadSeparatorInEmptyThread`. You can use this to enable a thread separator if the thread is empty. [#4629](https://github.com/GetStream/stream-chat-android/pull/4629)
- Added the `messageLimit` parameter to MessageListViewModel and MessageListViewModelFactory. [#4634](https://github.com/GetStream/stream-chat-android/pull/4634)
- Added the feature flag boolean `navigateToThreadViaNotification` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ internal class LoadNotificationDataWorker(
val getChannel = client.queryChannel(channelType, channelId, QueryChannelRequest())

val result = getChannel.zipWith(getMessage).await()

if (result.isSuccess) {
val (channel, message) = result.data()
val messageParentId = message.parentId

if (messageParentId != null) {
logger.v { "[doWork] fetching thread parent message." }
client.getMessage(messageParentId).await()
}

ChatClient.displayNotification(channel = channel, message = message)
Result.success()
} else {
Expand Down Expand Up @@ -120,7 +128,7 @@ internal class LoadNotificationDataWorker(
context: Context,
channelId: String,
channelType: String,
messageId: String
messageId: String,
) {
val syncMessagesWork = OneTimeWorkRequestBuilder<LoadNotificationDataWorker>()
.setInputData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.getstream.chat.android.client.api.models.QueryChannelsRequest
import io.getstream.chat.android.client.call.Call
import io.getstream.chat.android.client.call.CoroutineCall
import io.getstream.chat.android.client.errors.ChatError
import io.getstream.chat.android.client.events.ChatEventHandler
import io.getstream.chat.android.client.extensions.cidToTypeAndId
import io.getstream.chat.android.client.extensions.internal.isEphemeral
import io.getstream.chat.android.client.models.Attachment
Expand All @@ -37,6 +38,7 @@ import io.getstream.chat.android.client.utils.Result
import io.getstream.chat.android.client.utils.internal.validateCidWithResult
import io.getstream.chat.android.client.utils.map
import io.getstream.chat.android.client.utils.toResultError
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
import io.getstream.chat.android.offline.event.handler.chat.factory.ChatEventHandlerFactory
import io.getstream.chat.android.offline.extensions.internal.logic
Expand Down Expand Up @@ -144,6 +146,30 @@ public fun ChatClient.getRepliesAsState(
return requestsAsState(coroutineScope).getReplies(messageId, messageLimit)
}

/**
* Returns thread replies in the form of [ThreadState], however, unlike [getRepliesAsState]
* it will return it only after the API call made to get replies has ended. Thread state
* will be returned regardless if the API call has succeeded or failed, the only difference is
* in how up to date the replies in the thread state are.
*
* @param messageId The ID of the original message the replies were made to.
* @param messageLimit The number of messages that will be initially loaded.
* @param coroutineScope The [CoroutineScope] used for executing the request.
*
* @return [ThreadState] wrapped inside a [Call].
*/
@InternalStreamChatApi
@JvmOverloads
public fun ChatClient.getRepliesAsStateCall(
messageId: String,
messageLimit: Int,
coroutineScope: CoroutineScope = CoroutineScope(DispatcherProvider.IO),
): Call<ThreadState> {
return CoroutineCall(scope = state.scope) {
Result(requestsAsState(coroutineScope).getRepliesAsStateCall(messageId, messageLimit))
}
}

/**
* Provides an ease-of-use piece of functionality that checks if the user is available or not. If it's not, we don't emit
* any state, but rather return an empty StateFlow.
Expand Down Expand Up @@ -289,6 +315,29 @@ public fun ChatClient.cancelEphemeralMessage(message: Message): Call<Boolean> {
}
}

/**
* Attempts to fetch the message from offline cache before making an API call.
*
* @param messageId The id of the message we are fetching.
*
* @return The message with the corresponding iID wrapped inside a [Call].
*/
@InternalStreamChatApi
@CheckResult
public fun ChatClient.getMessageUsingCache(
messageId: String,
): Call<Message> {
return CoroutineCall(state.scope) {
val message = logic.getMessageById(messageId) ?: logic.getMessageByIdFromDb(messageId)

if (message != null) {
Result(data = message)
} else {
getMessage(messageId).await()
}
}
}

/**
* Loads message for a given message id and channel id.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ internal class ChannelLogic(
configs = configs,
users = users.values.toList(),
channels = listOf(channel),
messages = channel.messages
messages = channel.messages,
cacheForMessages = true
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import io.getstream.chat.android.client.plugin.listeners.ThreadQueryListener
import io.getstream.chat.android.offline.plugin.state.channel.thread.internal.ThreadMutableState

/** Logic class for thread state management. Implements [ThreadQueryListener] as listener for LLC requests. */
internal class ThreadLogic(private val threadStateLogic: ThreadStateLogic) {
internal class ThreadLogic(
private val threadStateLogic: ThreadStateLogic,
) {

private val mutableState: ThreadMutableState = threadStateLogic.writeThreadState()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,29 @@ internal class LogicRegistry internal constructor(
}
}

/**
* Attempts to fetch the message with the given ID from the mutable state.
*
* @see [getMessageByIdFromDb] if you need to search the database for the message as well.
*
* @param messageId The ID of the message we are attempting to retrieve.
*
* @return The message with the given id, if such exists, null otherwise.
*/
fun getMessageById(messageId: String): Message? {
return channelFromMessageId(messageId)?.getMessage(messageId)
?: threadFromMessageId(messageId)?.getMessage(messageId)
}

/**
* Attempts to fetch the message with the given ID from the mutable database.
*
* @param messageId The ID of the message we are attempting to retrieve.
*
* @return The message with the given id, if such exists, null otherwise.
*/
suspend fun getMessageByIdFromDb(messageId: String): Message? = repos.selectMessage(messageId)?.copy()

/**
* This method returns [ChannelLogic] if the messages passed is not only in a thread. Use this to avoid
* updating [ChannelLogic] for a messages that is only inside [ThreadLogic]. If you get null as a result,
Expand Down Expand Up @@ -177,6 +195,7 @@ internal class LogicRegistry internal constructor(
return threads.getOrPut(messageId) {
val mutableState = stateRegistry.mutableThread(messageId)
val stateLogic = ThreadStateLogic(mutableState)

ThreadLogic(stateLogic)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,23 @@ internal class ChatClientStateCalls(
chatClient.getReplies(messageId, messageLimit).launch(scope)
return state.thread(messageId)
}

/**
* Fetches replies from the backend and returns them in the form of a [ThreadState].
* Unlike [getReplies] which makes an API call and instantly returns [ThreadState], this function
* will wait for the API call completion and then return [ThreadState].
*
* This is useful in situations such as when we want to focus on the last message in a thread after a PN
* has been received, avoiding multiple [ThreadState] emissions simplifies handling it in the UI.
*
* @param messageId The id of the message we want to get replies for.
* @param messageLimit The upper limit of how many replies should be fetched.
*
* @return The replies in the form of [ThreadState].
*/
internal suspend fun getRepliesAsStateCall(messageId: String, messageLimit: Int): ThreadState {
logger.d { "getting replied for message with id: $messageId" }
chatClient.getReplies(messageId, messageLimit).await()
return state.thread(messageId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ class ChatFragment : Fragment() {

private val args: ChatFragmentArgs by navArgs()

private val factory: MessageListViewModelFactory by lazy { MessageListViewModelFactory(args.cid, args.messageId) }
private val factory: MessageListViewModelFactory by lazy {
MessageListViewModelFactory(
cid = args.cid,
messageId = args.messageId,
navigateToThreadViaNotification = true
)
}
private val chatViewModelFactory: ChatViewModelFactory by lazy { ChatViewModelFactory(args.cid) }
private val headerViewModel: MessageListHeaderViewModel by viewModels { factory }
private val messageListViewModel: MessageListViewModel by viewModels { factory }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ public final class com/getstream/sdk/chat/viewmodel/MessageInputViewModel : andr
public final class com/getstream/sdk/chat/viewmodel/messages/MessageListViewModel : androidx/lifecycle/ViewModel {
public static final field DEFAULT_MESSAGES_LIMIT I
public static final field SEPARATOR_TIME I
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;I)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;IZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;IZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getChannel ()Landroidx/lifecycle/LiveData;
public final fun getChannelState ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getDeletedMessageVisibility ()Landroidx/lifecycle/LiveData;
Expand Down Expand Up @@ -3772,7 +3772,8 @@ public final class io/getstream/chat/android/ui/message/list/viewmodel/factory/M
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;I)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;IZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;IZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel;
}

Expand All @@ -3782,6 +3783,7 @@ public final class io/getstream/chat/android/ui/message/list/viewmodel/factory/M
public final fun cid (Ljava/lang/String;)Lio/getstream/chat/android/ui/message/list/viewmodel/factory/MessageListViewModelFactory$Builder;
public final fun messageId (Ljava/lang/String;)Lio/getstream/chat/android/ui/message/list/viewmodel/factory/MessageListViewModelFactory$Builder;
public final fun messageLimit (I)Lio/getstream/chat/android/ui/message/list/viewmodel/factory/MessageListViewModelFactory$Builder;
public final fun navigateToThreadViaNotification (Z)Lio/getstream/chat/android/ui/message/list/viewmodel/factory/MessageListViewModelFactory$Builder;
}

public final class io/getstream/chat/android/ui/message/preview/MessagePreviewStyle {
Expand Down
Loading

0 comments on commit 950b84c

Please sign in to comment.