Merge pull request #6314 from vector-im/task/eric/replace_flatten_with_direct_parent
Replace flattenParents with directParentName
This commit is contained in:
commit
a2cf8720ab
1
changelog.d/6314.misc
Normal file
1
changelog.d/6314.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Improves performance on search screen by replacing flattenParents with directParentName in RoomSummary
|
@ -610,4 +610,82 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDirectParentNames() = runSessionTest(context()) { commonTestHelper ->
|
||||||
|
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
|
||||||
|
|
||||||
|
val spaceAInfo = createPublicSpace(
|
||||||
|
commonTestHelper,
|
||||||
|
aliceSession, "SpaceA",
|
||||||
|
listOf(
|
||||||
|
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("A2", true, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val spaceBInfo = createPublicSpace(
|
||||||
|
commonTestHelper,
|
||||||
|
aliceSession, "SpaceB",
|
||||||
|
listOf(
|
||||||
|
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("B2", true, true),
|
||||||
|
Triple("B3", true, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// also add B1 in space A
|
||||||
|
|
||||||
|
val B1roomId = spaceBInfo.roomIds.first()
|
||||||
|
val viaServers = listOf(aliceSession.sessionParams.homeServerHost ?: "")
|
||||||
|
|
||||||
|
val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId)
|
||||||
|
val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId)
|
||||||
|
commonTestHelper.runBlockingTest {
|
||||||
|
spaceA!!.addChildren(B1roomId, viaServers, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomSummary = aliceSession.getRoomSummary(B1roomId)
|
||||||
|
roomSummary != null &&
|
||||||
|
roomSummary.directParentNames.size == 2 &&
|
||||||
|
roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) &&
|
||||||
|
roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
|
||||||
|
roomSummary != null &&
|
||||||
|
roomSummary.directParentNames.size == 1 &&
|
||||||
|
roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newAName = "FooBar"
|
||||||
|
commonTestHelper.runBlockingTest {
|
||||||
|
spaceA!!.asRoom().stateService().updateName(newAName)
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomSummary = aliceSession.getRoomSummary(B1roomId)
|
||||||
|
roomSummary != null &&
|
||||||
|
roomSummary.directParentNames.size == 2 &&
|
||||||
|
roomSummary.directParentNames.contains(newAName) &&
|
||||||
|
roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
|
||||||
|
roomSummary != null &&
|
||||||
|
roomSummary.directParentNames.size == 1 &&
|
||||||
|
roomSummary.directParentNames.contains(newAName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,14 +243,11 @@ interface RoomService {
|
|||||||
* @param queryParams The filter to use
|
* @param queryParams The filter to use
|
||||||
* @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
|
* @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
|
||||||
* @param sortOrder defines how to sort the results
|
* @param sortOrder defines how to sort the results
|
||||||
* @param getFlattenParents When true, the list of known parents and grand parents summaries will be resolved.
|
|
||||||
* This can have significant impact on performance, better be used only on manageable list (filtered by displayName, ..).
|
|
||||||
*/
|
*/
|
||||||
fun getFilteredPagedRoomSummariesLive(
|
fun getFilteredPagedRoomSummariesLive(
|
||||||
queryParams: RoomSummaryQueryParams,
|
queryParams: RoomSummaryQueryParams,
|
||||||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
|
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
|
||||||
getFlattenParents: Boolean = false,
|
|
||||||
): UpdatableLivePageResult
|
): UpdatableLivePageResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,9 +164,9 @@ data class RoomSummary(
|
|||||||
*/
|
*/
|
||||||
val spaceChildren: List<SpaceChildInfo>? = null,
|
val spaceChildren: List<SpaceChildInfo>? = null,
|
||||||
/**
|
/**
|
||||||
* List of all the space parents. Will be empty by default, you have to explicitly request it.
|
* The names of the room's direct space parents if any.
|
||||||
*/
|
*/
|
||||||
val flattenParents: List<RoomSummary> = emptyList(),
|
val directParentNames: List<String> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* List of all the space parent Ids.
|
* List of all the space parent Ids.
|
||||||
*/
|
*/
|
||||||
|
@ -51,6 +51,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
|
|||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
|
||||||
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
|
||||||
import org.matrix.android.sdk.internal.util.Normalizer
|
import org.matrix.android.sdk.internal.util.Normalizer
|
||||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -59,7 +60,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
private val normalizer: Normalizer
|
private val normalizer: Normalizer
|
||||||
) : MatrixRealmMigration(
|
) : MatrixRealmMigration(
|
||||||
dbName = "Session",
|
dbName = "Session",
|
||||||
schemaVersion = 34L,
|
schemaVersion = 35L,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||||
@ -103,5 +104,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion < 32) MigrateSessionTo032(realm).perform()
|
if (oldVersion < 32) MigrateSessionTo032(realm).perform()
|
||||||
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
||||||
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
|
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
|
||||||
|
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,7 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||||||
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
|
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
directParentNames = roomSummaryEntity.directParentNames.toList(),
|
||||||
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
|
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
|
||||||
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
|
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
|
||||||
// I should probably use #hasEncryptorClassForAlgorithm but it says it supports
|
// I should probably use #hasEncryptorClassForAlgorithm but it says it supports
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.database.migration
|
||||||
|
|
||||||
|
import io.realm.DynamicRealm
|
||||||
|
import io.realm.RealmList
|
||||||
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
|
internal class MigrateSessionTo035(realm: DynamicRealm) : RealmMigrator(realm, 35) {
|
||||||
|
|
||||||
|
override fun doMigrate(realm: DynamicRealm) {
|
||||||
|
realm.schema.get("RoomSummaryEntity")
|
||||||
|
?.addRealmListField(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, String::class.java)
|
||||||
|
?.transform { it.setList(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, RealmList("")) }
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,8 @@ internal open class RoomSummaryEntity(
|
|||||||
@PrimaryKey var roomId: String = "",
|
@PrimaryKey var roomId: String = "",
|
||||||
var roomType: String? = null,
|
var roomType: String? = null,
|
||||||
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
|
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
|
||||||
var children: RealmList<SpaceChildSummaryEntity> = RealmList()
|
var children: RealmList<SpaceChildSummaryEntity> = RealmList(),
|
||||||
|
var directParentNames: RealmList<String> = RealmList(),
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
private var displayName: String? = ""
|
private var displayName: String? = ""
|
||||||
|
@ -152,9 +152,8 @@ internal class DefaultRoomService @Inject constructor(
|
|||||||
queryParams: RoomSummaryQueryParams,
|
queryParams: RoomSummaryQueryParams,
|
||||||
pagedListConfig: PagedList.Config,
|
pagedListConfig: PagedList.Config,
|
||||||
sortOrder: RoomSortOrder,
|
sortOrder: RoomSortOrder,
|
||||||
getFlattenParents: Boolean
|
|
||||||
): UpdatableLivePageResult {
|
): UpdatableLivePageResult {
|
||||||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents)
|
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
||||||
|
@ -200,14 +200,13 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||||||
queryParams: RoomSummaryQueryParams,
|
queryParams: RoomSummaryQueryParams,
|
||||||
pagedListConfig: PagedList.Config,
|
pagedListConfig: PagedList.Config,
|
||||||
sortOrder: RoomSortOrder,
|
sortOrder: RoomSortOrder,
|
||||||
getFlattenedParents: Boolean = false
|
|
||||||
): UpdatableLivePageResult {
|
): UpdatableLivePageResult {
|
||||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
roomSummariesQuery(realm, queryParams).process(sortOrder)
|
roomSummariesQuery(realm, queryParams).process(sortOrder)
|
||||||
}
|
}
|
||||||
val dataSourceFactory = realmDataSourceFactory.map {
|
val dataSourceFactory = realmDataSourceFactory.map {
|
||||||
roomSummaryMapper.map(it)
|
roomSummaryMapper.map(it)
|
||||||
}.map { if (getFlattenedParents) it.getWithParents() else it }
|
}
|
||||||
|
|
||||||
val boundaries = MutableLiveData(ResultBoundaries())
|
val boundaries = MutableLiveData(ResultBoundaries())
|
||||||
|
|
||||||
@ -246,13 +245,6 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomSummary.getWithParents(): RoomSummary {
|
|
||||||
val parents = flattenParentIds.mapNotNull { parentId ->
|
|
||||||
getRoomSummary(parentId)
|
|
||||||
}
|
|
||||||
return copy(flattenParents = parents)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
|
||||||
val liveRooms = monarchy.findAllManagedWithChanges {
|
val liveRooms = monarchy.findAllManagedWithChanges {
|
||||||
roomSummariesQuery(it, queryParams)
|
roomSummariesQuery(it, queryParams)
|
||||||
|
@ -223,6 +223,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
.sort(RoomSummaryEntityFields.ROOM_ID)
|
.sort(RoomSummaryEntityFields.ROOM_ID)
|
||||||
.findAll().map {
|
.findAll().map {
|
||||||
it.flattenParentIds = null
|
it.flattenParentIds = null
|
||||||
|
it.directParentNames.clear()
|
||||||
it to emptyList<RoomSummaryEntity>().toMutableSet()
|
it to emptyList<RoomSummaryEntity>().toMutableSet()
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
@ -350,39 +351,29 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val acyclicGraph = graph.withoutEdges(backEdges)
|
val acyclicGraph = graph.withoutEdges(backEdges)
|
||||||
// Timber.v("## SPACES: acyclicGraph $acyclicGraph")
|
|
||||||
val flattenSpaceParents = acyclicGraph.flattenDestination().map {
|
val flattenSpaceParents = acyclicGraph.flattenDestination().map {
|
||||||
it.key.name to it.value.map { it.name }
|
it.key.name to it.value.map { it.name }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
// Timber.v("## SPACES: flattenSpaceParents ${flattenSpaceParents.map { it.key.name to it.value.map { it.name } }.joinToString("\n") {
|
|
||||||
// it.first + ": [" + it.second.joinToString(",") + "]"
|
|
||||||
// }}")
|
|
||||||
|
|
||||||
// Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}")
|
|
||||||
|
|
||||||
lookupMap.entries
|
lookupMap.entries
|
||||||
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
|
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
|
||||||
.forEach { entry ->
|
.forEach { entry ->
|
||||||
val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
|
val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
// Timber.v("## SPACES: check hierarchy of ${parent.name} id ${parent.roomId}")
|
|
||||||
// Timber.v("## SPACES: flat known parents of ${parent.name} are ${flattenSpaceParents[parent.roomId]}")
|
|
||||||
val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId)
|
val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId)
|
||||||
// Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}")
|
|
||||||
entry.value.forEach { child ->
|
entry.value.forEach { child ->
|
||||||
RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
|
RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
|
||||||
|
childSum.directParentNames.add(parent.displayName())
|
||||||
|
|
||||||
// Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}")
|
if (childSum.flattenParentIds == null) {
|
||||||
// var allParents = childSum.flattenParentIds ?: ""
|
childSum.flattenParentIds = ""
|
||||||
if (childSum.flattenParentIds == null) childSum.flattenParentIds = ""
|
}
|
||||||
flattenParentsIds.forEach {
|
flattenParentsIds.forEach {
|
||||||
if (childSum.flattenParentIds?.contains(it) != true) {
|
if (childSum.flattenParentIds?.contains(it) != true) {
|
||||||
childSum.flattenParentIds += "|$it"
|
childSum.flattenParentIds += "|$it"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// childSum.flattenParentIds = "$allParents|"
|
|
||||||
|
|
||||||
// Timber.v("## SPACES: flatten of ${childSum.name} is ${childSum.flattenParentIds}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,7 @@ class RoomListSectionBuilder(
|
|||||||
},
|
},
|
||||||
{ queryParams ->
|
{ queryParams ->
|
||||||
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
||||||
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams, getFlattenParents = true)
|
val updatableFilterLivePageResult = session.roomService().getFilteredPagedRoomSummariesLive(queryParams)
|
||||||
onUpdatable(updatableFilterLivePageResult)
|
onUpdatable(updatableFilterLivePageResult)
|
||||||
|
|
||||||
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
|
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
|
||||||
|
@ -207,9 +207,18 @@ class RoomSummaryItemFactory @Inject constructor(
|
|||||||
|
|
||||||
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
|
private fun getSearchResultSubtitle(roomSummary: RoomSummary): String {
|
||||||
val userId = roomSummary.directUserId
|
val userId = roomSummary.directUserId
|
||||||
val spaceName = roomSummary.flattenParents.lastOrNull()?.name
|
val directParent = joinParentNames(roomSummary)
|
||||||
val canonicalAlias = roomSummary.canonicalAlias
|
val canonicalAlias = roomSummary.canonicalAlias
|
||||||
|
|
||||||
return (userId ?: spaceName ?: canonicalAlias).orEmpty()
|
return (userId ?: directParent ?: canonicalAlias).orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun joinParentNames(roomSummary: RoomSummary) = with(roomSummary) {
|
||||||
|
when (val size = directParentNames.size) {
|
||||||
|
0 -> null
|
||||||
|
1 -> directParentNames.first()
|
||||||
|
2 -> stringProvider.getString(R.string.search_space_two_parents, directParentNames[0], directParentNames[1])
|
||||||
|
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -764,6 +764,11 @@
|
|||||||
<string name="search_members_hint">Filter room members</string>
|
<string name="search_members_hint">Filter room members</string>
|
||||||
<string name="search_banned_user_hint">Filter banned users</string>
|
<string name="search_banned_user_hint">Filter banned users</string>
|
||||||
<string name="search_no_results">No results</string>
|
<string name="search_no_results">No results</string>
|
||||||
|
<string name="search_space_two_parents">%1$s and %2$s</string>
|
||||||
|
<plurals name="search_space_multiple_parents">
|
||||||
|
<item quantity="one">%1$s and %2$d other</item>
|
||||||
|
<item quantity="other">%1$s and %2$d others</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- home room settings -->
|
<!-- home room settings -->
|
||||||
<string name="room_settings_all_messages">All messages</string>
|
<string name="room_settings_all_messages">All messages</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user