-
Notifications
You must be signed in to change notification settings - Fork 273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PBE-3749] Implement ThreadList state and UI #5441
base: develop
Are you sure you want to change the base?
Conversation
@CheckResult | ||
@InternalStreamChatApi | ||
public fun queryThreadsResult(query: QueryThreadsRequest): Call<QueryThreadsResult> { | ||
return queryThreadsInternal(query) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this extra method, we can call the API directly from here
return queryThreadsInternal(query) | |
return api.queryThreads(query) |
val cid: String, | ||
val channelInfo: ChannelInfo, | ||
val channel: Channel?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we keep ChannelInfo
instance here?
Instead of having a nullable Channel
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I was able to see from the API responses, we are getting a full Channel
object, but again, it is not marked as mandatory in the API specs. Should we still keep this as a mandatory ChannelInfo
? (The Channel
object is later used to resolve the channel name in ThreadItem
via the channelNameFormatter
)
val parentMessageId: String, | ||
val parentMessage: Message, | ||
val parentMessage: Message?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A thread should always start with a parentMessage
, it shouldn't be null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I followed the swagger api spec where the parent_message
field was not marked as mandatory. But you are right, this will probably never be null, I will update this.
val createdBy: User?, | ||
val replyCount: Int?, | ||
val participantCount: Int?, | ||
val threadParticipants: List<User>?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our domain model should avoid nullability as far as possible.
On the case of collections, we should fallback to emptyCollections instead.
val replyCount: Int?, | ||
val participantCount: Int?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is backend sending us those properties as null??
They shouldn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the 'parent_message' param, but I will revert this change, as it doesn't make sense to get nulls here.
val oldThreads = getThreads() | ||
val oldThread = oldThreads.find { it.parentMessageId == parent.id } | ||
oldThread ?: return false // no matching parent message was found | ||
val newThread = oldThread.copy( | ||
parentMessage = parent, | ||
deletedAt = parent.deletedAt, | ||
updatedAt = parent.updatedAt, | ||
replyCount = parent.replyCount, | ||
) | ||
val newThreads = oldThreads.map { | ||
if (it.parentMessageId == newThread.parentMessageId) { | ||
newThread | ||
} else { | ||
it | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of performing a search and after mapping all elements, what about only mapping elements?
The copy()
method can be invoke within the map lambda
val thread = oldThreads.find { it.parentMessageId == reply.parentId } | ||
thread ?: return | ||
val oldReplies = thread.latestReplies | ||
val newReplies = if (oldReplies.any { it.id == reply.id }) { | ||
// update | ||
oldReplies.map { | ||
if (it.id == reply.id) { | ||
reply | ||
} else { | ||
it | ||
} | ||
} | ||
} else { | ||
// insert | ||
oldReplies + listOf(reply) | ||
} | ||
val sortedNewReplies = newReplies.sortedBy { | ||
it.createdAt ?: it.createdLocallyAt | ||
} | ||
val newThread = thread.copy(latestReplies = sortedNewReplies) | ||
val newThreads = oldThreads.map { | ||
if (it.parentMessageId == newThread.parentMessageId) { | ||
newThread | ||
} else { | ||
it | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
private companion object { | ||
private val INITIAL_STATE = ThreadListState( | ||
threads = emptyList(), | ||
isLoading = true, | ||
isLoadingMore = false, | ||
unseenThreadsCount = 0, | ||
) | ||
private const val QUERY_LIMIT = 25 | ||
private const val QUERY_REPLY_LIMIT = 10 | ||
private const val QUERY_PARTICIPANT_LIMIT = 10 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we move companion object
to the end of the class?
private const val QUERY_LIMIT = 25 | ||
private const val QUERY_REPLY_LIMIT = 10 | ||
private const val QUERY_PARTICIPANT_LIMIT = 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those constants should be the default ones, but allowing our customers to inject their own one by the constructor
public val state: StateFlow<ThreadListState> | ||
get() = _state | ||
|
||
private val scope = CoroutineScope(DispatcherProvider.Main + SupervisorJob()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't create scopes internally in our classes.
It should be injected.
On that case I think we should use the VM scope
# Conflicts: # stream-chat-android-compose/src/main/res/values/strings.xml
# Conflicts: # stream-chat-android-compose/src/main/res/values/strings.xml
🎯 Goal
This PR is the first request related to the Threads V2 feature. It covers the
ThreadList
component, and the logic fetching the data behind it.🛠 Implementation details
QueryThreadsRequest
to accept additional params as per the documentationQueryThreadsResponseDto
to include theprev
andnext
cursorsChatApi.queryThreads
method to returnQueryThreadsResult
instead ofList<Thread>
(now includes theprev
andnext
cursors)DownstreamThreadDto
to include some additional fields delivered from back-end, and make some of the fields optional (as per the endpoint documentation)Thread
model based on the changes ofDownstreamThreadDto
ChatEvent
changes:NotificationThreadMessageNewEventDto
event of type:notification.thread_message_new
. This event is now delivered when a new reply is added to a thread.NotificationThreadMessageNew
event based onNotificationThreadMessageNewEventDto
ChannelLogic
StatePlugin
changes:QueryThreadsListener
for applying side effects to theChatClient.queryThreads
operation.QueryThreadsListenerState
- Implementation of theQueryThreadsListener
applying side effects as part of theStatePlugin
QueryThreadsLogic
-> single point of entry for the new threads-related logic. Handles the logic of updating the threads-related state caused by theChatClient.queryThreads
and byChatEvents
. (Accessible via theLogicRegistry
)QueryThreadsState/QueryThreadsMutableState
-> state holder for the new threads-related data (Accessible via theStateRegistry
).QueryThreadsStateLogic
-> interactor between theQueryThreadsLogic
and theQueryThreadsMutableState
ChatClient.queryThreadsAsState
-> extension which initializes and exposes the 'global'QueryThreadsState
ThreadListController
-> a controller to be shared by theXML
andCompose
SDKs which handles theload
andloadMore
requests for the thread list, and exposes a reactive render-ableThreadListState
.ThreadListViewModel
backed by theThreadListController
ThreadList
backed byThreadListViewModel
which shows the list of threads, and the optional unread threads banner (with default thread item, unread threads banner and default loading and empty states)🎨 UI Changes
🧪 Testing
This is currently not testable with the sample app (I will commit the updates to the sample app once this PR is approved)
Provide a patch below if it is necessary for testing
Provide the patch summary here
☑️Contributor Checklist
General
develop
branchCode & documentation
☑️Reviewer Checklist
🎉 GIF
Please provide a suitable gif that describes your work on this pull request