Skip to content

Commit

Permalink
[AND-25] Threads V2 offline support (#5481)
Browse files Browse the repository at this point in the history
* [PBE-3749] Update ThreadsApi to match the definition.

* [PBE-3749] Register "notification.thread_message_new" EventType.

* [PBE-3749] Implement initial state-management for 'Query Threads'.

* [PBE-3749] Implement ThreadList component.

* [PBE-3749] Implement 'Threads' tab in compose sample app.

* [PBE-3749] FIx pagination logic and add a threshold.

* [PBE-3749] Add queryThreads preconditions checks.

* [PBE-3749] Revert ktlint commit.

* [PBE-3749] Remove redundant state update in ThreadListController.

* [PBE-3749] Add handling for different ChatEvents.

* [PBE-3749] Remove redundant coroutine creation and docs.

* [PBE-3749] Fix detekt and spotless.

* Revert "[PBE-3749] Implement 'Threads' tab in compose sample app."

This reverts commit fdb1ac1

* Revert "Revert "[PBE-3749] Implement 'Threads' tab in compose sample app.""

This reverts commit fbc9b3d.

* Revert "[PBE-3749] Implement 'Threads' tab in compose sample app."

This reverts commit fdb1ac1

* [PBE-3749] Hide threads-related public apis.

* [PBE-3749] Fix PR remarks related DTOs.

* [PBE-3749] Fix wrong composable preview.

* [PBE-3749] Use inheritScope to create ThreadListController coroutine scope.

* [PBE-3749] Implement ChatClient::markThreadRead operation.

* [PBE-3749] Update CHANGELOG for markThreadRead.

* [PBE-3749] Fix failing test.

* [PBE-3749] Separate `markThreadRead` from `markRead`.

* [PBE-3749] Implement unreadThreads logic as part of the GlobalState.

* [PBE-3749] Add GlobalState::unreadThreadsCount to CHANGELOG.md.

* [PBE-3749] Add marking thread as read handling.

* [PBE-3749] Fix incrementing unread count for new thread messages.

* [PBE-3749] Add ThreadItem customization options.

* [PBE-3749] Make Threads API public.

* [PBE-3749] Add Threads tab to compose sample app.

* [PBE-3749] Add threads state tests.

* [PBE-3749] Suppress LongMethod warning.

* [PBE-3749] Add ChatClient::markThreadUnread.

* [PBE-3749] Add ChatClient::markThreadUnread to CHANGELOG.md.

* [PBE-3749] Add stateless ThreadList.

* [PBE-3749] Add ThreadList to CHANGELOG and add docusaurus documentation
.

* [PBE-3749] Ensure threads state is updated on different client operations.

* [PBE-3749] Fix failing tests.

* [PBE-3749] Add 'Mark thread as unread' handling.

* [PBE-3749] Add Threads offline support.

* [PBE-3749] apply spotless.

* [PBE-3749] Post-merge clean-up.

* [PBE-3749] Remove docs.

* [PBE-3749] Update NotificationMarkUnreadEvent with threadId for thread events.

* [PBE-3749] Add threads offline support to CHANGELOG.md.

* [AND-25] Delegate Thread::replyCount to Thread::parentMessage.

---------

Co-authored-by: PetarVelikov <[email protected]>
  • Loading branch information
VelikovPetar and PetarVelikov authored Nov 20, 2024
1 parent 42a3aed commit f06acf7
Show file tree
Hide file tree
Showing 41 changed files with 1,351 additions and 297 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
### ⬆️ Improved

### ✅ Added
- Add support for Threads. [#5481](https://github.com/GetStream/stream-chat-android/pull/5481)

### ⚠️ Changed

Expand Down
25 changes: 20 additions & 5 deletions stream-chat-android-client/api/stream-chat-android-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -1766,14 +1766,15 @@ public final class io/getstream/chat/android/client/events/NotificationMarkReadE
}

public final class io/getstream/chat/android/client/events/NotificationMarkUnreadEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasUnreadCounts, io/getstream/chat/android/client/events/UserEvent {
public fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;I)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;I)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()I
public final fun component11 ()Ljava/lang/String;
public final fun component12 ()Ljava/util/Date;
public final fun component13 ()Ljava/lang/String;
public final fun component14 ()I
public final fun component14 ()Ljava/lang/String;
public final fun component15 ()I
public final fun component2 ()Ljava/util/Date;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lio/getstream/chat/android/models/User;
Expand All @@ -1782,8 +1783,8 @@ public final class io/getstream/chat/android/client/events/NotificationMarkUnrea
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()I
public final fun component9 ()I
public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;I)Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;IILjava/lang/Object;)Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;
public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;I)Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Lio/getstream/chat/android/models/User;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIILjava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lio/getstream/chat/android/client/events/NotificationMarkUnreadEvent;
public fun equals (Ljava/lang/Object;)Z
public fun getChannelId ()Ljava/lang/String;
public fun getChannelType ()Ljava/lang/String;
Expand All @@ -1793,6 +1794,7 @@ public final class io/getstream/chat/android/client/events/NotificationMarkUnrea
public final fun getLastReadMessageAt ()Ljava/util/Date;
public final fun getLastReadMessageId ()Ljava/lang/String;
public fun getRawCreatedAt ()Ljava/lang/String;
public final fun getThreadId ()Ljava/lang/String;
public fun getTotalUnreadCount ()I
public fun getType ()Ljava/lang/String;
public fun getUnreadChannels ()I
Expand Down Expand Up @@ -2547,6 +2549,18 @@ public abstract interface class io/getstream/chat/android/client/persistance/rep
public abstract fun selectSyncState (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class io/getstream/chat/android/client/persistance/repository/ThreadsRepository {
public abstract fun clear (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun deleteChannelThreads (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun insertThreadOrder (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun insertThreads (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun selectThread (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun selectThreadOrder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun selectThreads (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun upsertMessageInThread (Lio/getstream/chat/android/models/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun upsertMessagesInThread (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class io/getstream/chat/android/client/persistance/repository/UserRepository {
public abstract fun clear (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun insertCurrentUser (Lio/getstream/chat/android/models/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -2564,6 +2578,7 @@ public abstract interface class io/getstream/chat/android/client/persistance/rep
public abstract fun createQueryChannelsRepository ()Lio/getstream/chat/android/client/persistance/repository/QueryChannelsRepository;
public abstract fun createReactionRepository (Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/client/persistance/repository/ReactionRepository;
public abstract fun createSyncStateRepository ()Lio/getstream/chat/android/client/persistance/repository/SyncStateRepository;
public abstract fun createThreadsRepository (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/client/persistance/repository/ThreadsRepository;
public abstract fun createUserRepository ()Lio/getstream/chat/android/client/persistance/repository/UserRepository;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ private fun NotificationMarkUnreadEventDto.toDomain(currentUserId: UserId?): Not
lastReadMessageId = last_read_message_id,
lastReadMessageAt = last_read_at.date,
unreadMessages = unread_messages,
threadId = thread_id,
unreadThreads = unread_threads,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ internal fun DownstreamThreadDto.toDomain(currentUserId: UserId?): Thread =
parentMessage = parent_message.toDomain(currentUserId),
createdByUserId = created_by_user_id,
createdBy = created_by?.toDomain(currentUserId),
replyCount = reply_count,
participantCount = participant_count,
threadParticipants = thread_participants.orEmpty().map { it.toDomain(currentUserId) },
lastMessageAt = last_message_at,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ internal data class NotificationMarkUnreadEventDto(
val unread_messages: Int,
val total_unread_count: Int,
val unread_channels: Int,
val thread_id: String? = null,
val unread_threads: Int = 0,
) : ChatEventDto()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import java.util.Date
* @param parent_message: The parent message.
* @param created_by_user_id: The ID of the user who created the thread.
* @param created_by: The user who created the thread.
* @param reply_count: The number of replies in the thread.
* @param participant_count: The number of participants in the thread.
* @param thread_participants: The participants in the thread.
* @param last_message_at: The date of the last message in the thread.
Expand All @@ -50,7 +49,6 @@ internal data class DownstreamThreadDto(
val parent_message: DownstreamMessageDto,
val created_by_user_id: String,
val created_by: DownstreamUserDto?,
val reply_count: Int,
val participant_count: Int,
val thread_participants: List<DownstreamThreadParticipantDto>?,
val last_message_at: Date,
Expand All @@ -77,7 +75,6 @@ internal data class DownstreamThreadDto(
* @param parent_message_id: The parent message ID.
* @param participant_count: The number of participants in the thread.
* @param reply_count: The number of replies in the thread.
* @param thread_participants: The participants in the thread.
* @param title: The title of the thread.
* @param updated_at: The date when the thread was updated.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ public data class NotificationMarkUnreadEvent(
val firstUnreadMessageId: String,
val lastReadMessageAt: Date,
val lastReadMessageId: String?,
val threadId: String? = null,
val unreadThreads: Int = 0,
) : CidEvent(), UserEvent, HasUnreadCounts

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.extensions.internal

import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.models.ChannelUserRead
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.Thread
import io.getstream.chat.android.models.ThreadInfo
import io.getstream.chat.android.models.ThreadParticipant
import io.getstream.chat.android.models.User
import java.util.Date

/**
* Updates the given Thread with the new message (parent or reply).
*/
@InternalStreamChatApi
public fun Thread.updateParentOrReply(message: Message): Thread {
return when (this.parentMessageId) {
message.id -> updateParent(parent = message)
message.parentId -> upsertReply(reply = message)
else -> this
}
}

/**
* Updates the parent message of a Thread.
*/
@InternalStreamChatApi
public fun Thread.updateParent(parent: Message): Thread {
// Skip update if [parent] is not related to this Thread
if (this.parentMessageId != parent.id) return this
return this.copy(
parentMessage = parent,
deletedAt = parent.deletedAt,
updatedAt = parent.updatedAt ?: this.updatedAt,
)
}

/**
* Inserts a new reply (or updates and existing one) in a Thread.
*/
@InternalStreamChatApi
public fun Thread.upsertReply(reply: Message): Thread {
// Ship update if [reply] is not related to this Thread
if (this.parentMessageId != reply.parentId) return this
val newReplies = upsertMessageInList(reply, this.latestReplies)
val isInsert = newReplies.size > this.latestReplies.size
val sortedNewReplies = newReplies.sortedBy {
it.createdAt ?: it.createdLocallyAt
}
val lastMessageAt = sortedNewReplies.lastOrNull()?.let { latestReply ->
latestReply.createdAt ?: latestReply.createdLocallyAt
}
// The new message could be from a new thread participant
val threadParticipants = if (isInsert) {
upsertThreadParticipantInList(
newParticipant = ThreadParticipant(user = reply.user),
participants = this.threadParticipants,
)
} else {
this.threadParticipants
}
val participantCount = threadParticipants.size
// Update read counts (+1 for each non-sender of the message)
val read = if (isInsert) {
updateReadCounts(this.read, reply)
} else {
this.read
}
return this.copy(
lastMessageAt = lastMessageAt ?: this.lastMessageAt,
updatedAt = lastMessageAt ?: this.updatedAt,
participantCount = participantCount,
threadParticipants = threadParticipants,
latestReplies = sortedNewReplies,
read = read,
)
}

/**
* Marks the given thread as read by the given user.
*
* @param threadInfo The [ThreadInfo] holding info about the [Thread] which should be marked as read.
* @param user The [User] for which the thread should be marked as read.
* @param createdAt The [Date] of the 'mark read' event.
*/
@InternalStreamChatApi
public fun Thread.markAsReadByUser(threadInfo: ThreadInfo, user: User, createdAt: Date): Thread {
// Skip update if [threadInfo] is not related to this Thread
if (this.parentMessageId != threadInfo.parentMessageId) return this
val updatedRead = this.read.map { read ->
if (read.user.id == user.id) {
read.copy(
user = user,
unreadMessages = 0,
lastReceivedEventDate = createdAt,
)
} else {
read
}
}
return this.copy(
activeParticipantCount = threadInfo.activeParticipantCount,
deletedAt = threadInfo.deletedAt,
lastMessageAt = threadInfo.lastMessageAt ?: this.lastMessageAt,
parentMessage = threadInfo.parentMessage ?: this.parentMessage,
participantCount = threadInfo.participantCount,
title = threadInfo.title,
updatedAt = threadInfo.updatedAt,
read = updatedRead,
)
}

/**
* Marks the given thread as unread by the given user.
*
* @param user The [User] for which the thread should be marked as read.
* @param createdAt The [Date] of the 'mark read' event.
*/
@InternalStreamChatApi
public fun Thread.markAsUnreadByUser(user: User, createdAt: Date): Thread {
val updatedRead = this.read.map { read ->
if (read.user.id == user.id) {
read.copy(
user = user,
// Update this value to what the backend returns (when implemented)
unreadMessages = read.unreadMessages + 1,
lastReceivedEventDate = createdAt,
)
} else {
read
}
}
return this.copy(read = updatedRead)
}

private fun upsertMessageInList(newMessage: Message, messages: List<Message>): List<Message> {
// Insert
if (messages.none { it.id == newMessage.id }) {
return messages + listOf(newMessage)
}
// Update
return messages.map { message ->
if (message.id == newMessage.id) {
newMessage
} else {
message
}
}
}

private fun upsertThreadParticipantInList(
newParticipant: ThreadParticipant,
participants: List<ThreadParticipant>,
): List<ThreadParticipant> {
// Insert
if (participants.none { it.getUserId() == newParticipant.getUserId() }) {
return participants + listOf(newParticipant)
}
// Update
return participants.map { participant ->
if (participant.getUserId() == newParticipant.getUserId()) {
newParticipant
} else {
participant
}
}
}

private fun updateReadCounts(read: List<ChannelUserRead>, reply: Message): List<ChannelUserRead> {
return read.map { userRead ->
if (userRead.user.id != reply.user.id) {
userRead.copy(unreadMessages = userRead.unreadMessages + 1)
} else {
userRead
}
}
}
Loading

0 comments on commit f06acf7

Please sign in to comment.