-
Notifications
You must be signed in to change notification settings - Fork 278
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[AND-25] Threads V2 offline support (#5481)
* [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
1 parent
42a3aed
commit f06acf7
Showing
41 changed files
with
1,351 additions
and
297 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 193 additions & 0 deletions
193
...droid-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
Oops, something went wrong.