From bd955eec32daad8ece75cad4d3311815b7faba8b Mon Sep 17 00:00:00 2001
From: Taras Smakula <tarassmakula@gmail.com>
Date: Tue, 14 Nov 2023 17:30:51 +0200
Subject: [PATCH] Add blur for circles

---
 .../feature/circles/CirclesDataSource.kt      | 42 +++++++++++++++++--
 .../feature/circles/CirclesFragment.kt        |  3 ++
 .../feature/circles/CirclesViewModel.kt       |  8 +++-
 .../circles/list/CirclesListAdapter.kt        |  6 ++-
 .../feature/circles/list/CirclesViewHolder.kt | 18 ++++----
 .../circles/mapping/RoomSummaryMapping.kt     |  5 ++-
 .../org/futo/circles/model/CircleListItem.kt  |  1 +
 .../res/layout/list_item_invited_circle.xml   | 17 ++++++++
 8 files changed, 84 insertions(+), 16 deletions(-)

diff --git a/app/src/main/java/org/futo/circles/feature/circles/CirclesDataSource.kt b/app/src/main/java/org/futo/circles/feature/circles/CirclesDataSource.kt
index 184b79801..ad3c3d1a1 100644
--- a/app/src/main/java/org/futo/circles/feature/circles/CirclesDataSource.kt
+++ b/app/src/main/java/org/futo/circles/feature/circles/CirclesDataSource.kt
@@ -2,9 +2,12 @@ package org.futo.circles.feature.circles
 
 import androidx.lifecycle.asFlow
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.withContext
+import org.futo.circles.core.extensions.getKnownUsersFlow
 import org.futo.circles.core.feature.workspace.SharedCircleDataSource
 import org.futo.circles.core.feature.workspace.SpacesTreeAccountDataSource
 import org.futo.circles.core.model.CIRCLES_SPACE_ACCOUNT_DATA_KEY
@@ -25,18 +28,34 @@ class CirclesDataSource @Inject constructor(
     private val sharedCircleDataSource: SharedCircleDataSource
 ) {
 
+    private val roomIdsToUnblurProfile = MutableStateFlow<Set<String>>(emptySet())
+
     fun getCirclesFlow() = combine(
         MatrixSessionProvider.getSessionOrThrow().roomService()
             .getRoomSummariesLive(roomSummaryQueryParams { excludeType = null })
             .asFlow(),
+        MatrixSessionProvider.getSessionOrThrow().getKnownUsersFlow(),
+        roomIdsToUnblurProfile,
         MatrixSessionProvider.getSessionOrThrow().roomService().getChangeMembershipsLive().asFlow()
-    ) { roomSummaries, _ ->
-        withContext(Dispatchers.IO) { buildCirclesList(roomSummaries) }
+    ) { roomSummaries, knownUsers, roomIdsToUnblur, _ ->
+        withContext(Dispatchers.IO) {
+            buildCirclesList(
+                roomSummaries,
+                knownUsers.map { it.userId }.toSet(),
+                roomIdsToUnblur
+            )
+        }
     }.distinctUntilChanged()
 
-    private fun buildCirclesList(list: List<RoomSummary>): List<CircleListItem> {
+    private fun buildCirclesList(
+        list: List<RoomSummary>,
+        knownUsersIds: Set<String>,
+        roomIdsToUnblur: Set<String>
+    ): List<CircleListItem> {
         val invites =
-            list.filter { isInviteToCircleTimeline(it) }.map { it.toInviteCircleListItem() }
+            list.filter { isInviteToCircleTimeline(it) }.map { it.toInviteCircleListItem(
+                shouldBlurIconFor(it, knownUsersIds, roomIdsToUnblur)
+            ) }
 
         val joinedCirclesSpaceIds = getJoinedCirclesIds()
         val joinedCircles = list.filter { isJoinedCircle(it, joinedCirclesSpaceIds) }
@@ -88,4 +107,19 @@ class CirclesDataSource @Inject constructor(
             addAll(items)
         }
     }
+
+    private fun shouldBlurIconFor(
+        roomSummary: RoomSummary,
+        knownUserIds: Set<String>,
+        roomIdsToUnblur: Set<String>
+    ): Boolean {
+        val isKnownUser = knownUserIds.contains(roomSummary.inviterId)
+        val isRoomUnbluredByUser = roomIdsToUnblur.contains(roomSummary.roomId)
+        val hasIcon = roomSummary.avatarUrl.isNotEmpty()
+        return !isKnownUser && !isRoomUnbluredByUser && hasIcon
+    }
+
+    fun unblurProfileImageFor(id: String) {
+        roomIdsToUnblurProfile.update { set -> set.toMutableSet().apply { add(id) } }
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt b/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt
index 73b29c4ea..079fe7cad 100644
--- a/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt
+++ b/app/src/main/java/org/futo/circles/feature/circles/CirclesFragment.kt
@@ -81,6 +81,9 @@ class CirclesFragment : Fragment(org.futo.circles.core.R.layout.fragment_rooms),
                 onRoomClicked = { roomListItem -> onRoomListItemClicked(roomListItem) },
                 onInviteClicked = { roomListItem, isAccepted ->
                     onInviteClicked(roomListItem, isAccepted)
+                },
+                onUnblurProfileIconClicked = { roomListItem ->
+                    viewModel.unblurProfileIcon(roomListItem)
                 }
             ).also { listAdapter = it }
             bindToFab(binding.fbAddRoom)
diff --git a/app/src/main/java/org/futo/circles/feature/circles/CirclesViewModel.kt b/app/src/main/java/org/futo/circles/feature/circles/CirclesViewModel.kt
index abd3ae344..bf1bfeb1e 100644
--- a/app/src/main/java/org/futo/circles/feature/circles/CirclesViewModel.kt
+++ b/app/src/main/java/org/futo/circles/feature/circles/CirclesViewModel.kt
@@ -15,12 +15,14 @@ import org.futo.circles.core.feature.room.invite.InviteRequestsDataSource
 import org.futo.circles.core.model.LoadingData
 import org.futo.circles.core.provider.MatrixSessionProvider
 import org.futo.circles.core.utils.getTimelineRoomFor
+import org.futo.circles.model.CircleListItem
+import org.futo.circles.model.GroupListItem
 import org.matrix.android.sdk.api.session.getRoomSummary
 import javax.inject.Inject
 
 @HiltViewModel
 class CirclesViewModel @Inject constructor(
-    dataSource: CirclesDataSource,
+    private val dataSource: CirclesDataSource,
     private val inviteRequestsDataSource: InviteRequestsDataSource,
     private val createRoomDataSource: CreateRoomDataSource
 ) : ViewModel() {
@@ -55,4 +57,8 @@ class CirclesViewModel @Inject constructor(
         }
     }
 
+    fun unblurProfileIcon(roomListItem: CircleListItem) {
+        dataSource.unblurProfileImageFor(roomListItem.id)
+    }
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/feature/circles/list/CirclesListAdapter.kt b/app/src/main/java/org/futo/circles/feature/circles/list/CirclesListAdapter.kt
index 4d123814b..bd1d6fa24 100644
--- a/app/src/main/java/org/futo/circles/feature/circles/list/CirclesListAdapter.kt
+++ b/app/src/main/java/org/futo/circles/feature/circles/list/CirclesListAdapter.kt
@@ -12,7 +12,8 @@ enum class CirclesListItemViewType { JoinedCircle, InvitedCircle, Header }
 
 class CirclesListAdapter(
     private val onRoomClicked: (CircleListItem) -> Unit,
-    private val onInviteClicked: (CircleListItem, Boolean) -> Unit
+    private val onInviteClicked: (CircleListItem, Boolean) -> Unit,
+    private val onUnblurProfileIconClicked: (CircleListItem) -> Unit
 ) : BaseRvAdapter<CircleListItem, CirclesViewHolder>(PayloadIdEntityCallback { old, new ->
     if (new is JoinedCircleListItem && old is JoinedCircleListItem) {
         CircleListItemPayload(
@@ -44,6 +45,9 @@ class CirclesListAdapter(
             parent = parent,
             onInviteClicked = { position, isAccepted ->
                 onInviteClicked(getItem(position), isAccepted)
+            },
+            onShowProfileIconClicked = { position ->
+                onUnblurProfileIconClicked(getItem(position))
             }
         )
 
diff --git a/app/src/main/java/org/futo/circles/feature/circles/list/CirclesViewHolder.kt b/app/src/main/java/org/futo/circles/feature/circles/list/CirclesViewHolder.kt
index 85495bd21..c5ac09278 100644
--- a/app/src/main/java/org/futo/circles/feature/circles/list/CirclesViewHolder.kt
+++ b/app/src/main/java/org/futo/circles/feature/circles/list/CirclesViewHolder.kt
@@ -2,7 +2,6 @@ package org.futo.circles.feature.circles.list
 
 import android.view.View
 import android.view.ViewGroup
-import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
 import org.futo.circles.R
@@ -23,10 +22,6 @@ import org.futo.circles.model.JoinedCircleListItem
 abstract class CirclesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
     abstract fun bind(data: CircleListItem)
 
-    protected fun setIcon(groupIcon: ImageView, avatarUrl: String?, title: String) {
-        groupIcon.loadProfileIcon(avatarUrl, title)
-    }
-
     protected fun setTitle(titleView: TextView, title: String) {
         titleView.text = title
     }
@@ -50,7 +45,7 @@ class JoinedCircleViewHolder(
         if (data !is JoinedCircleListItem) return
 
         with(binding) {
-            setIcon(ivCircle, data.info.avatarUrl, data.info.title)
+            ivCircle.loadProfileIcon(data.info.avatarUrl, data.info.title)
             setTitle(tvCircleTitle, data.info.title)
             setFollowingCount(data.followingCount)
             setFollowedByCount(data.followedByCount)
@@ -89,7 +84,8 @@ class JoinedCircleViewHolder(
 
 class InvitedCircleViewHolder(
     parent: ViewGroup,
-    onInviteClicked: (Int, Boolean) -> Unit
+    onInviteClicked: (Int, Boolean) -> Unit,
+    onShowProfileIconClicked: (Int) -> Unit
 ) : CirclesViewHolder(inflate(parent, ListItemInvitedCircleBinding::inflate)) {
 
     private companion object : ViewBindingHolder
@@ -99,13 +95,19 @@ class InvitedCircleViewHolder(
     init {
         onClick(binding.btnAccept) { position -> onInviteClicked(position, true) }
         onClick(binding.btnDecline) { position -> onInviteClicked(position, false) }
+        onClick(binding.ivCircle) { position -> onShowProfileIconClicked(position) }
     }
 
     override fun bind(data: CircleListItem) {
         if (data !is InvitedCircleListItem) return
 
         with(binding) {
-            setIcon(ivCircle, data.info.avatarUrl, data.info.title)
+            tvShowProfileImage.setIsVisible(data.shouldBlurIcon)
+            ivCircle.loadProfileIcon(
+                data.info.avatarUrl,
+                data.info.title,
+                applyBlur = data.shouldBlurIcon
+            )
             setTitle(tvCircleTitle, data.info.title)
             binding.tvInvitedBy.text =
                 context.getString(
diff --git a/app/src/main/java/org/futo/circles/mapping/RoomSummaryMapping.kt b/app/src/main/java/org/futo/circles/mapping/RoomSummaryMapping.kt
index d29d694aa..724e2c14e 100644
--- a/app/src/main/java/org/futo/circles/mapping/RoomSummaryMapping.kt
+++ b/app/src/main/java/org/futo/circles/mapping/RoomSummaryMapping.kt
@@ -47,10 +47,11 @@ fun RoomSummary.toJoinedCircleListItem(isShared: Boolean = false) = JoinedCircle
     knockRequestsCount = getKnocksCount(getTimelineRoomFor(roomId)?.roomId ?: "")
 )
 
-fun RoomSummary.toInviteCircleListItem() = InvitedCircleListItem(
+fun RoomSummary.toInviteCircleListItem(shouldBlurIcon: Boolean) = InvitedCircleListItem(
     id = roomId,
     info = toRoomInfo(),
-    inviterName = getInviterName()
+    inviterName = getInviterName(),
+    shouldBlurIcon = shouldBlurIcon
 )
 
 private fun RoomSummary.getFollowersCount(): Int =
diff --git a/app/src/main/java/org/futo/circles/model/CircleListItem.kt b/app/src/main/java/org/futo/circles/model/CircleListItem.kt
index e847980f2..b11467fed 100644
--- a/app/src/main/java/org/futo/circles/model/CircleListItem.kt
+++ b/app/src/main/java/org/futo/circles/model/CircleListItem.kt
@@ -38,5 +38,6 @@ data class InvitedCircleListItem(
     override val id: String,
     override val info: RoomInfo,
     val inviterName: String,
+    val shouldBlurIcon: Boolean
 ) : CircleRoomListItem(id, info, Membership.INVITE)
 
diff --git a/app/src/main/res/layout/list_item_invited_circle.xml b/app/src/main/res/layout/list_item_invited_circle.xml
index 3f6dec433..cc1d80790 100644
--- a/app/src/main/res/layout/list_item_invited_circle.xml
+++ b/app/src/main/res/layout/list_item_invited_circle.xml
@@ -24,6 +24,23 @@
         app:strokeWidth="1dp"
         tools:src="@color/blue" />
 
+    <TextView
+        android:id="@+id/tvShowProfileImage"
+        style="@style/body"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:drawablePadding="-4dp"
+        android:text="@string/show"
+        android:textColor="@color/white"
+        android:visibility="gone"
+        app:drawableTint="@color/white"
+        app:drawableTopCompat="@drawable/ic_seen"
+        app:layout_constraintBottom_toBottomOf="@id/ivCircle"
+        app:layout_constraintEnd_toEndOf="@id/ivCircle"
+        app:layout_constraintStart_toStartOf="@id/ivCircle"
+        app:layout_constraintTop_toTopOf="@id/ivCircle"
+        tools:visibility="visible" />
+
 
     <TextView
         android:id="@+id/tvCircleTitle"
-- 
GitLab