mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-03 11:07:30 +01:00
parent
d153083819
commit
dfbc3eea3b
@ -1,13 +1,50 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.usecase.GetUnreadItemsUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultInboxCoordinator : InboxCoordinator {
|
||||
internal class DefaultInboxCoordinator(
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val getUnreadItemsUseCase: GetUnreadItemsUseCase,
|
||||
) : InboxCoordinator {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
override val unreadOnly = MutableStateFlow(true)
|
||||
override val effects = MutableSharedFlow<InboxMviModel.Effect>()
|
||||
override val unreadReplies = MutableStateFlow(0)
|
||||
override val unreadMentions = MutableStateFlow(0)
|
||||
override val unreadMessages = MutableStateFlow(0)
|
||||
override val totalUnread = combine(
|
||||
unreadMentions,
|
||||
unreadMessages,
|
||||
unreadReplies,
|
||||
) { res1, res2, res3 ->
|
||||
res1 + res2 + res3
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0
|
||||
)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
identityRepository.isLogged.onEach {
|
||||
updateCounters()
|
||||
}.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUnreadOnly(value: Boolean) {
|
||||
unreadOnly.value = value
|
||||
@ -16,4 +53,21 @@ internal class DefaultInboxCoordinator : InboxCoordinator {
|
||||
override suspend fun emitEffect(effect: InboxMviModel.Effect) {
|
||||
effects.emit(effect)
|
||||
}
|
||||
|
||||
override suspend fun updateUnreadCount(): Int {
|
||||
updateCounters()
|
||||
return totalUnread.value
|
||||
}
|
||||
|
||||
private suspend fun updateCounters() {
|
||||
if (!identityRepository.authToken.value.isNullOrEmpty()) {
|
||||
unreadMentions.value = getUnreadItemsUseCase.getUnreadMentions()
|
||||
unreadReplies.value = getUnreadItemsUseCase.getUnreadReplies()
|
||||
unreadMessages.value = getUnreadItemsUseCase.getUnreadMessages()
|
||||
} else {
|
||||
unreadReplies.value = 0
|
||||
unreadMentions.value = 0
|
||||
unreadMessages.value = 0
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,14 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
interface InboxCoordinator {
|
||||
val unreadOnly: StateFlow<Boolean>
|
||||
val effects: SharedFlow<InboxMviModel.Effect>
|
||||
val unreadReplies: StateFlow<Int>
|
||||
val unreadMentions: StateFlow<Int>
|
||||
val unreadMessages: StateFlow<Int>
|
||||
val totalUnread: StateFlow<Int>
|
||||
|
||||
fun setUnreadOnly(value: Boolean)
|
||||
|
||||
suspend fun emitEffect(effect: InboxMviModel.Effect)
|
||||
|
||||
suspend fun updateUnreadCount(): Int
|
||||
}
|
||||
|
@ -11,11 +11,23 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.InboxMess
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.InboxMessagesViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.usecase.DefaultGetUnreadItemsUseCase
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.usecase.GetUnreadItemsUseCase
|
||||
import org.koin.dsl.module
|
||||
|
||||
val inboxTabModule = module {
|
||||
single<InboxCoordinator> {
|
||||
DefaultInboxCoordinator()
|
||||
DefaultInboxCoordinator(
|
||||
identityRepository = get(),
|
||||
getUnreadItemsUseCase = get(),
|
||||
)
|
||||
}
|
||||
single<GetUnreadItemsUseCase> {
|
||||
DefaultGetUnreadItemsUseCase(
|
||||
identityRepository = get(),
|
||||
userRepository = get(),
|
||||
messageRepository = get(),
|
||||
)
|
||||
}
|
||||
factory<InboxMviModel> {
|
||||
InboxViewModel(
|
||||
@ -24,7 +36,6 @@ val inboxTabModule = module {
|
||||
userRepository = get(),
|
||||
coordinator = get(),
|
||||
settingsRepository = get(),
|
||||
contentResetCoordinator = get(),
|
||||
)
|
||||
}
|
||||
factory<InboxRepliesMviModel> {
|
||||
@ -34,7 +45,6 @@ val inboxTabModule = module {
|
||||
identityRepository = get(),
|
||||
siteRepository = get(),
|
||||
commentRepository = get(),
|
||||
messageRepository = get(),
|
||||
themeRepository = get(),
|
||||
settingsRepository = get(),
|
||||
hapticFeedback = get(),
|
||||
@ -48,7 +58,6 @@ val inboxTabModule = module {
|
||||
userRepository = get(),
|
||||
identityRepository = get(),
|
||||
commentRepository = get(),
|
||||
messageRepository = get(),
|
||||
themeRepository = get(),
|
||||
settingsRepository = get(),
|
||||
hapticFeedback = get(),
|
||||
@ -64,7 +73,6 @@ val inboxTabModule = module {
|
||||
messageRepository = get(),
|
||||
coordinator = get(),
|
||||
notificationCenter = get(),
|
||||
userRepository = get(),
|
||||
settingsRepository = get(),
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,9 @@ interface InboxMviModel :
|
||||
val isLogged: Boolean? = null,
|
||||
val section: InboxSection = InboxSection.Replies,
|
||||
val unreadOnly: Boolean = true,
|
||||
val unreadReplies: Int = 0,
|
||||
val unreadMentions: Int = 0,
|
||||
val unreadMessages: Int = 0,
|
||||
)
|
||||
|
||||
sealed interface Effect {
|
||||
|
@ -173,9 +173,30 @@ object InboxScreen : Tab {
|
||||
SectionSelector(
|
||||
modifier = Modifier.padding(vertical = Spacing.s),
|
||||
titles = listOf(
|
||||
stringResource(MR.strings.inbox_section_replies),
|
||||
stringResource(MR.strings.inbox_section_mentions),
|
||||
stringResource(MR.strings.inbox_section_messages),
|
||||
buildString {
|
||||
append(stringResource(MR.strings.inbox_section_replies))
|
||||
if (uiState.unreadReplies > 0) {
|
||||
append(" (")
|
||||
append(uiState.unreadReplies)
|
||||
append(")")
|
||||
}
|
||||
},
|
||||
buildString {
|
||||
append(stringResource(MR.strings.inbox_section_mentions))
|
||||
if (uiState.unreadMentions > 0) {
|
||||
append(" (")
|
||||
append(uiState.unreadMentions)
|
||||
append(")")
|
||||
}
|
||||
},
|
||||
buildString {
|
||||
append(stringResource(MR.strings.inbox_section_messages))
|
||||
if (uiState.unreadMessages > 0) {
|
||||
append(" (")
|
||||
append(uiState.unreadMessages)
|
||||
append(")")
|
||||
}
|
||||
},
|
||||
),
|
||||
currentSection = when (uiState.section) {
|
||||
InboxSection.Mentions -> 1
|
||||
|
@ -2,7 +2,6 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.ContentResetCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toInboxUnreadOnly
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
@ -20,16 +19,26 @@ class InboxViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val coordinator: InboxCoordinator,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val contentResetCoordinator: ContentResetCoordinator,
|
||||
) : InboxMviModel,
|
||||
MviModel<InboxMviModel.Intent, InboxMviModel.UiState, InboxMviModel.Effect> by mvi {
|
||||
|
||||
override fun onStarted() {
|
||||
mvi.onStarted()
|
||||
mvi.scope?.launch {
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
identityRepository.isLogged.onEach { logged ->
|
||||
mvi.updateState { it.copy(isLogged = logged) }
|
||||
}.launchIn(this)
|
||||
|
||||
coordinator.unreadMentions.onEach { value ->
|
||||
mvi.updateState { it.copy(unreadMentions = value) }
|
||||
}.launchIn(this)
|
||||
coordinator.unreadReplies.onEach { value ->
|
||||
mvi.updateState { it.copy(unreadReplies = value) }
|
||||
}.launchIn(this)
|
||||
coordinator.unreadMessages.onEach { value ->
|
||||
mvi.updateState { it.copy(unreadMessages = value) }
|
||||
}.launchIn(this)
|
||||
|
||||
val settingsUnreadOnly =
|
||||
settingsRepository.currentSettings.value.defaultInboxType.toInboxUnreadOnly()
|
||||
if (uiState.value.unreadOnly != settingsUnreadOnly) {
|
||||
|
@ -11,7 +11,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.Ident
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
||||
@ -28,7 +27,6 @@ class InboxMentionsViewModel(
|
||||
private val commentRepository: CommentRepository,
|
||||
private val themeRepository: ThemeRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val messageRepository: PrivateMessageRepository,
|
||||
private val hapticFeedback: HapticFeedback,
|
||||
private val coordinator: InboxCoordinator,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
@ -296,21 +294,7 @@ class InboxMentionsViewModel(
|
||||
|
||||
private fun updateUnreadItems() {
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
val auth = identityRepository.authToken.value
|
||||
val unreadCount = if (!auth.isNullOrEmpty()) {
|
||||
val mentionCount =
|
||||
userRepository.getMentions(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val replyCount =
|
||||
userRepository.getReplies(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val messageCount =
|
||||
messageRepository.getAll(auth, page = 1, limit = 50).orEmpty().groupBy {
|
||||
listOf(it.creator?.id ?: 0, it.recipient?.id ?: 0).sorted()
|
||||
.joinToString()
|
||||
}.count()
|
||||
mentionCount + replyCount
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val unreadCount = coordinator.updateUnreadCount()
|
||||
mvi.emitEffect(InboxMentionsMviModel.Effect.UpdateUnreadItems(unreadCount))
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.Ident
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.otherUser
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -23,7 +22,6 @@ class InboxMessagesViewModel(
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val siteRepository: SiteRepository,
|
||||
private val messageRepository: PrivateMessageRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val coordinator: InboxCoordinator,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
@ -62,6 +60,8 @@ class InboxMessagesViewModel(
|
||||
changeUnreadOnly(value)
|
||||
}
|
||||
}
|
||||
|
||||
updateUnreadItems()
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,21 +149,7 @@ class InboxMessagesViewModel(
|
||||
|
||||
private fun updateUnreadItems() {
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
val auth = identityRepository.authToken.value
|
||||
val unreadCount = if (!auth.isNullOrEmpty()) {
|
||||
val mentionCount =
|
||||
userRepository.getMentions(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val replyCount =
|
||||
userRepository.getReplies(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val messageCount =
|
||||
messageRepository.getAll(auth, page = 1, limit = 50).orEmpty().groupBy {
|
||||
listOf(it.creator?.id ?: 0, it.recipient?.id ?: 0).sorted()
|
||||
.joinToString()
|
||||
}.count()
|
||||
mentionCount + replyCount + messageCount
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val unreadCount = coordinator.updateUnreadCount()
|
||||
mvi.emitEffect(InboxMessagesMviModel.Effect.UpdateUnreadItems(unreadCount))
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.Ident
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||
@ -28,7 +27,6 @@ class InboxRepliesViewModel(
|
||||
private val userRepository: UserRepository,
|
||||
private val siteRepository: SiteRepository,
|
||||
private val commentRepository: CommentRepository,
|
||||
private val messageRepository: PrivateMessageRepository,
|
||||
private val themeRepository: ThemeRepository,
|
||||
private val hapticFeedback: HapticFeedback,
|
||||
private val coordinator: InboxCoordinator,
|
||||
@ -302,21 +300,7 @@ class InboxRepliesViewModel(
|
||||
}
|
||||
|
||||
private suspend fun updateUnreadItems() {
|
||||
val auth = identityRepository.authToken.value
|
||||
val unreadCount = if (!auth.isNullOrEmpty()) {
|
||||
val mentionCount =
|
||||
userRepository.getMentions(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val replyCount =
|
||||
userRepository.getReplies(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val messageCount =
|
||||
messageRepository.getAll(auth, page = 1, limit = 50).orEmpty().groupBy {
|
||||
listOf(it.creator?.id ?: 0, it.recipient?.id ?: 0).sorted()
|
||||
.joinToString()
|
||||
}.count()
|
||||
mentionCount + replyCount
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val unreadCount = coordinator.updateUnreadCount()
|
||||
mvi.emitEffect(InboxRepliesMviModel.Effect.UpdateUnreadItems(unreadCount))
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.usecase
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||
|
||||
class DefaultGetUnreadItemsUseCase(
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val messageRepository: PrivateMessageRepository,
|
||||
) : GetUnreadItemsUseCase {
|
||||
override suspend fun getUnreadReplies(): Int {
|
||||
val auth = identityRepository.authToken.value
|
||||
return userRepository.getReplies(auth, page = 1, limit = 50).orEmpty().count()
|
||||
}
|
||||
|
||||
override suspend fun getUnreadMentions(): Int {
|
||||
val auth = identityRepository.authToken.value
|
||||
return userRepository.getMentions(auth, page = 1, limit = 50).orEmpty().count()
|
||||
}
|
||||
|
||||
override suspend fun getUnreadMessages(): Int {
|
||||
val auth = identityRepository.authToken.value
|
||||
return messageRepository.getAll(auth, page = 1, limit = 50).orEmpty().groupBy {
|
||||
listOf(it.creator?.id ?: 0, it.recipient?.id ?: 0).sorted()
|
||||
.joinToString()
|
||||
}.count()
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.usecase
|
||||
|
||||
interface GetUnreadItemsUseCase {
|
||||
suspend fun getUnreadReplies(): Int
|
||||
suspend fun getUnreadMentions(): Int
|
||||
suspend fun getUnreadMessages(): Int
|
||||
}
|
@ -2,20 +2,15 @@ package com.github.diegoberaldin.raccoonforlemmy
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainViewModel(
|
||||
private val mvi: DefaultMviModel<MainScreenMviModel.Intent, MainScreenMviModel.UiState, MainScreenMviModel.Effect>,
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val messageRepository: PrivateMessageRepository,
|
||||
private val inboxCoordinator: InboxCoordinator,
|
||||
) : MainScreenMviModel,
|
||||
MviModel<MainScreenMviModel.Intent, MainScreenMviModel.UiState, MainScreenMviModel.Effect> by mvi {
|
||||
|
||||
@ -23,27 +18,9 @@ class MainViewModel(
|
||||
mvi.onStarted()
|
||||
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
identityRepository.isLogged.onEach { logged ->
|
||||
val unreadCount = if (logged == true) {
|
||||
val auth = identityRepository.authToken.value
|
||||
val mentionCount =
|
||||
userRepository.getMentions(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val replyCount =
|
||||
userRepository.getReplies(auth, page = 1, limit = 50).orEmpty().count()
|
||||
val messageCount =
|
||||
messageRepository.getAll(auth, page = 1, limit = 50).orEmpty().groupBy {
|
||||
listOf(it.creator?.id ?: 0, it.recipient?.id ?: 0).sorted()
|
||||
.joinToString()
|
||||
}.count()
|
||||
mentionCount + replyCount + messageCount
|
||||
} else {
|
||||
0
|
||||
}
|
||||
mvi.emitEffect(MainScreenMviModel.Effect.UnreadItemsDetected(unreadCount))
|
||||
}.launchIn(this)
|
||||
inboxCoordinator.totalUnread.onEach { unreadCount ->
|
||||
mvi.emitEffect(MainScreenMviModel.Effect.UnreadItemsDetected(unreadCount))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,7 @@ internal val internalSharedModule = module {
|
||||
factory<MainScreenMviModel> {
|
||||
MainViewModel(
|
||||
mvi = DefaultMviModel(MainScreenMviModel.UiState()),
|
||||
identityRepository = get(),
|
||||
userRepository = get(),
|
||||
messageRepository = get(),
|
||||
inboxCoordinator = get(),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user