From 0149d8f44f3fc5bc20eafcb2257642793cae40f8 Mon Sep 17 00:00:00 2001 From: Taras Smakula <tarassmakula@gmail.com> Date: Thu, 7 Mar 2024 17:14:19 +0200 Subject: [PATCH] Add loading indicators for invites --- .../feature/room/invites/InvitesViewModel.kt | 61 +++++++++++++++++-- .../room/invites/list/InvitesViewHolder.kt | 15 +++-- .../futo/circles/core/model/InviteListItem.kt | 7 ++- .../layout/list_item_connection_invite.xml | 25 +++++++- .../res/layout/list_item_invited_circle.xml | 23 +++++++ .../res/layout/list_item_invited_gallery.xml | 23 +++++++ .../res/layout/list_item_invited_group.xml | 23 +++++++ .../res/layout/list_item_people_request.xml | 23 +++++++ 8 files changed, 187 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/futo/circles/core/feature/room/invites/InvitesViewModel.kt b/core/src/main/java/org/futo/circles/core/feature/room/invites/InvitesViewModel.kt index eafb2308f..aae308101 100644 --- a/core/src/main/java/org/futo/circles/core/feature/room/invites/InvitesViewModel.kt +++ b/core/src/main/java/org/futo/circles/core/feature/room/invites/InvitesViewModel.kt @@ -1,5 +1,7 @@ package org.futo.circles.core.feature.room.invites +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData @@ -11,7 +13,12 @@ import org.futo.circles.core.extensions.launchBg import org.futo.circles.core.feature.room.invite.ManageInviteRequestsDataSource import org.futo.circles.core.feature.workspace.SharedCircleDataSource import org.futo.circles.core.model.CircleRoomTypeArg +import org.futo.circles.core.model.ConnectionInviteListItem +import org.futo.circles.core.model.FollowRequestListItem +import org.futo.circles.core.model.InviteHeader +import org.futo.circles.core.model.InviteListItem import org.futo.circles.core.model.InviteTypeArg +import org.futo.circles.core.model.RoomInviteListItem import javax.inject.Inject @HiltViewModel @@ -25,29 +32,57 @@ class InvitesViewModel @Inject constructor( private val inviteType: InviteTypeArg = savedStateHandle.getOrThrow("type") val inviteResultLiveData = SingleEventLiveData<Response<Unit?>>() - val invitesLiveData = dataSource.getInvitesFlow(inviteType).asLiveData() + private val loadingItemsIdsList = MutableLiveData<Set<String>>(emptySet()) + val invitesLiveData = MediatorLiveData<List<InviteListItem>>().also { + it.addSource(loadingItemsIdsList) { loadingItemsValue -> + val currentList = it.value ?: emptyList() + it.postValue(currentList.map { item -> + when (item) { + is ConnectionInviteListItem -> item.copy( + isLoading = loadingItemsValue.contains(item.id) + ) + + is FollowRequestListItem -> item.copy( + isLoading = loadingItemsValue.contains(item.id) + ) + + is RoomInviteListItem -> item.copy( + isLoading = loadingItemsValue.contains(item.id) + ) + + is InviteHeader -> item + } + }) + } + it.addSource(dataSource.getInvitesFlow(inviteType).asLiveData()) { value -> + it.postValue(value) + } + } fun getInviteType() = inviteType fun rejectRoomInvite(roomId: String) { launchBg { + toggleItemLoading(roomId) val result = manageInviteRequestsDataSource.rejectInvite(roomId) - inviteResultLiveData.postValue(result) + postInviteResult(result, roomId) } } fun acceptRoomInvite(roomId: String, roomType: CircleRoomTypeArg) { launchBg { + toggleItemLoading(roomId) val result = manageInviteRequestsDataSource.acceptInvite(roomId, roomType) - inviteResultLiveData.postValue(result) + postInviteResult(result, roomId) } } fun onFollowRequestAnswered(userId: String, accepted: Boolean) { launchBg { + toggleItemLoading(userId) val result = if (accepted) dataSource.acceptFollowRequest(userId) else dataSource.declineFollowRequest(userId) - inviteResultLiveData.postValue(result) + postInviteResult(result, userId) } } @@ -58,11 +93,25 @@ class InvitesViewModel @Inject constructor( fun onConnectionInviteAnswered(roomId: String, accepted: Boolean) { if (accepted) launchBg { - val result = sharedCircleDataSource.acceptSharedCircleInvite(roomId) - inviteResultLiveData.postValue(result) + toggleItemLoading(roomId) + val result = sharedCircleDataSource.acceptSharedCircleInvite(roomId) + postInviteResult(result, roomId) } else rejectRoomInvite(roomId) } + private fun postInviteResult(result: Response<Unit?>, id: String) { + inviteResultLiveData.postValue(result) + toggleItemLoading(id) + } + + private fun toggleItemLoading(id: String) { + val currentSet = loadingItemsIdsList.value?.toMutableSet() ?: return + val newLoadingSet = currentSet.apply { + if (this.contains(id)) remove(id) + else add(id) + } + loadingItemsIdsList.postValue(newLoadingSet) + } } \ No newline at end of file diff --git a/core/src/main/java/org/futo/circles/core/feature/room/invites/list/InvitesViewHolder.kt b/core/src/main/java/org/futo/circles/core/feature/room/invites/list/InvitesViewHolder.kt index ce4878c32..34acd22cc 100644 --- a/core/src/main/java/org/futo/circles/core/feature/room/invites/list/InvitesViewHolder.kt +++ b/core/src/main/java/org/futo/circles/core/feature/room/invites/list/InvitesViewHolder.kt @@ -49,6 +49,7 @@ class InvitedGroupViewHolder( if (data !is RoomInviteListItem) return with(binding) { + lLoading.setIsVisible(data.isLoading) ivGroup.loadRoomProfileIcon( data.info.avatarUrl, data.info.title, @@ -85,6 +86,7 @@ class InvitedCircleViewHolder( if (data !is RoomInviteListItem) return with(binding) { + lLoading.setIsVisible(data.isLoading) tvShowProfileImage.setIsVisible(data.shouldBlurIcon) ivCircle.loadRoomProfileIcon( data.info.avatarUrl, @@ -121,6 +123,7 @@ class InvitedGalleryViewHolder( if (data !is RoomInviteListItem) return with(binding) { + lLoading.setIsVisible(data.isLoading) tvGalleryTitle.text = data.info.title ivGallery.loadRoomProfileIcon( data.info.avatarUrl, @@ -148,8 +151,10 @@ class FollowRequestViewHolder( } override fun bind(data: InviteListItem) { - val user = (data as? FollowRequestListItem)?.user ?: return - bindUser(user) + if (data !is FollowRequestListItem) return + + binding.lLoading.setIsVisible(data.isLoading) + bindUser(data.user) binding.tvReasonMessage.apply { setIsVisible(data.reasonMessage != null) text = data.reasonMessage @@ -179,8 +184,10 @@ class ConnectionInviteViewHolder( } override fun bind(data: InviteListItem) { - val user = (data as? ConnectionInviteListItem)?.user ?: return - bindUser(user) + if (data !is ConnectionInviteListItem) return + + binding.lLoading.setIsVisible(data.isLoading) + bindUser(data.user) } private fun bindUser(user: CirclesUserSummary) { diff --git a/core/src/main/java/org/futo/circles/core/model/InviteListItem.kt b/core/src/main/java/org/futo/circles/core/model/InviteListItem.kt index 7cd4a690a..be79cb77f 100644 --- a/core/src/main/java/org/futo/circles/core/model/InviteListItem.kt +++ b/core/src/main/java/org/futo/circles/core/model/InviteListItem.kt @@ -28,17 +28,20 @@ data class RoomInviteListItem( val info: RoomInfo, val isEncrypted: Boolean, val inviterName: String, - val shouldBlurIcon: Boolean + val shouldBlurIcon: Boolean, + val isLoading: Boolean = false ) : InviteListItem(roomId) data class FollowRequestListItem( val user: CirclesUserSummary, - val reasonMessage: String? + val reasonMessage: String?, + val isLoading: Boolean = false ) : InviteListItem(user.id) data class ConnectionInviteListItem( val roomId: String, val user: CirclesUserSummary, + val isLoading: Boolean = false ) : InviteListItem(roomId) fun RoomSummary.toRoomInviteListItem(roomType: CircleRoomTypeArg, shouldBlurIcon: Boolean) = diff --git a/core/src/main/res/layout/list_item_connection_invite.xml b/core/src/main/res/layout/list_item_connection_invite.xml index 1e9d9a371..893f0199f 100644 --- a/core/src/main/res/layout/list_item_connection_invite.xml +++ b/core/src/main/res/layout/list_item_connection_invite.xml @@ -47,7 +47,7 @@ app:layout_constraintStart_toStartOf="@id/tvUserName" app:layout_constraintTop_toBottomOf="@id/tvUserName" /> - + <com.google.android.material.button.MaterialButton android:id="@+id/btnAccept" style="@style/AccentButtonStyle" @@ -78,4 +78,27 @@ app:layout_constraintStart_toEndOf="@id/btnAccept" app:layout_constraintTop_toTopOf="@id/btnAccept" /> + <FrameLayout + android:id="@+id/lLoading" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:outlineProvider="none" + android:translationZ="2dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivUserImage" + app:layout_constraintTop_toBottomOf="@+id/tvInvitesToConnect"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </FrameLayout> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/core/src/main/res/layout/list_item_invited_circle.xml b/core/src/main/res/layout/list_item_invited_circle.xml index cc1d80790..ba08bb8ff 100644 --- a/core/src/main/res/layout/list_item_invited_circle.xml +++ b/core/src/main/res/layout/list_item_invited_circle.xml @@ -70,6 +70,7 @@ app:layout_constraintTop_toBottomOf="@id/tvCircleTitle" tools:text="texsdt" /> + <com.google.android.material.button.MaterialButton android:id="@+id/btnAccept" style="@style/AccentButtonStyle" @@ -96,5 +97,27 @@ app:layout_constraintStart_toEndOf="@id/btnAccept" app:layout_constraintTop_toBottomOf="@+id/tvInvitedBy" /> + <FrameLayout + android:id="@+id/lLoading" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:outlineProvider="none" + android:translationZ="2dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivCircle" + app:layout_constraintTop_toBottomOf="@+id/tvInvitedBy"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/core/src/main/res/layout/list_item_invited_gallery.xml b/core/src/main/res/layout/list_item_invited_gallery.xml index 93c89ce3a..e3491a4af 100644 --- a/core/src/main/res/layout/list_item_invited_gallery.xml +++ b/core/src/main/res/layout/list_item_invited_gallery.xml @@ -90,4 +90,27 @@ app:layout_constraintStart_toEndOf="@id/btnAccept" app:layout_constraintTop_toBottomOf="@+id/tvInviterName" /> + <FrameLayout + android:id="@+id/lLoading" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:outlineProvider="none" + android:translationZ="2dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivGallery" + app:layout_constraintTop_toBottomOf="@+id/tvInviterName"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </FrameLayout> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/core/src/main/res/layout/list_item_invited_group.xml b/core/src/main/res/layout/list_item_invited_group.xml index 4cf4a5d99..8b3e3f612 100644 --- a/core/src/main/res/layout/list_item_invited_group.xml +++ b/core/src/main/res/layout/list_item_invited_group.xml @@ -101,4 +101,27 @@ app:layout_constraintStart_toEndOf="@id/btnAccept" app:layout_constraintTop_toBottomOf="@+id/tvInviterName" /> + <FrameLayout + android:id="@+id/lLoading" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:outlineProvider="none" + android:translationZ="2dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivGroup" + app:layout_constraintTop_toBottomOf="@+id/tvInviterName"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </FrameLayout> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/core/src/main/res/layout/list_item_people_request.xml b/core/src/main/res/layout/list_item_people_request.xml index 7315a0483..a70971190 100644 --- a/core/src/main/res/layout/list_item_people_request.xml +++ b/core/src/main/res/layout/list_item_people_request.xml @@ -93,4 +93,27 @@ app:layout_constraintStart_toEndOf="@id/btnAccept" app:layout_constraintTop_toTopOf="@id/btnAccept" /> + <FrameLayout + android:id="@+id/lLoading" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:outlineProvider="none" + android:translationZ="2dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivUserImage" + app:layout_constraintTop_toBottomOf="@+id/tvReasonMessage"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </FrameLayout> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file -- GitLab