mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
Update DisplayNameActivity
This commit is contained in:
parent
1068c167a3
commit
76166b39a9
@ -5,7 +5,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
@ -335,14 +335,13 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
|
||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
|
||||
implementation 'androidx.compose.ui:ui:1.4.3'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
|
||||
implementation 'androidx.compose.ui:ui:1.5.1'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.5.1'
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta"
|
||||
implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
|
||||
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02'
|
||||
implementation 'androidx.compose.material:material:1.5.0-alpha02'
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.1'
|
||||
implementation 'androidx.compose.material:material:1.5.1'
|
||||
}
|
||||
|
||||
static def getLastCommitTimestamp() {
|
||||
|
@ -115,7 +115,7 @@
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.onboarding.DisplayNameActivity"
|
||||
android:name="org.thoughtcrime.securesms.onboarding.name.DisplayNameActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
|
@ -41,7 +41,7 @@ class AlbumThumbnailView : RelativeLayout {
|
||||
private var slides: List<Slide> = listOf()
|
||||
private var slideSize: Int = 0
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
|
@ -30,9 +30,7 @@ class ThumbnailProgressBar: View {
|
||||
private val objectRect = Rect()
|
||||
private val drawingRect = Rect()
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas?) {
|
||||
if (canvas == null) return
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
getDrawingRect(objectRect)
|
||||
drawingRect.set(objectRect)
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityDisplayNameBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
binding = ActivityDisplayNameBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
with(binding) {
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
displayNameEditText.setOnEditorActionListener(
|
||||
OnEditorActionListener { _, actionID, event ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionID == EditorInfo.IME_ACTION_DONE ||
|
||||
(event.action == KeyEvent.ACTION_DOWN &&
|
||||
event.keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
register()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val displayName = binding.displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (displayName.toByteArray().size > ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH) {
|
||||
return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0)
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
val intent = Intent(this, PNModeActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import network.loki.messenger.databinding.ActivityLandingBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.onboarding.name.DisplayNameActivity
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.onboarding.name.DisplayNameActivity
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.ProgressArc
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
|
@ -0,0 +1,139 @@
|
||||
package org.thoughtcrime.securesms.onboarding.name
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityDisplayNameBinding
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.onboarding.PNModeActivity
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.base
|
||||
import org.thoughtcrime.securesms.ui.baseBold
|
||||
import org.thoughtcrime.securesms.ui.colorDestructive
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
private val viewModel: DisplayNameViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
|
||||
ComposeView(this)
|
||||
.apply { setContent { DisplayNameScreen(viewModel) } }
|
||||
.let(::setContentView)
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.eventFlow.collect {
|
||||
// val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
// inputMethodManager.hideSoftInputFromWindow(content.displayNameEditText.windowToken, 0)
|
||||
|
||||
Intent(this@DisplayNameActivity, PNModeActivity::class.java).also(::push)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayNameScreen(viewModel: DisplayNameViewModel) {
|
||||
val state = viewModel.stateFlow.collectAsState()
|
||||
|
||||
AppTheme {
|
||||
DisplayName(state.value, viewModel::onChange, viewModel::onContinue)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewDisplayName() {
|
||||
PreviewTheme(R.style.Classic_Dark) {
|
||||
DisplayName(State())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayName(state: State, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 50.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(stringResource(state.title), style = MaterialTheme.typography.h4)
|
||||
Text(
|
||||
stringResource(state.description),
|
||||
style = MaterialTheme.typography.base,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.displayName,
|
||||
onValueChange = { onChange(it) },
|
||||
placeholder = { Text(stringResource(R.string.activity_display_name_edit_text_hint)) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
textColor = state.error?.let { colorDestructive } ?:
|
||||
LocalContentColor.current.copy(LocalContentAlpha.current),
|
||||
focusedBorderColor = Color(0xff414141),
|
||||
unfocusedBorderColor = Color(0xff414141),
|
||||
cursorColor = LocalContentColor.current,
|
||||
placeholderColor = state.error?.let { colorDestructive }
|
||||
?: MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
|
||||
),
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { viewModel.onContinue() },
|
||||
onGo = { viewModel.onContinue() },
|
||||
onSearch = { viewModel.onContinue() },
|
||||
onSend = { viewModel.onContinue() },
|
||||
),
|
||||
isError = state.error != null,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
state.error?.let {
|
||||
Text(stringResource(it), style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
OutlineButton(
|
||||
stringResource(R.string.continue_2),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.width(262.dp)
|
||||
) { onContinue() }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.onboarding.name
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class DisplayNameViewModel @Inject constructor(
|
||||
private val prefs: TextSecurePreferences
|
||||
): ViewModel() {
|
||||
|
||||
private val state = MutableStateFlow(State())
|
||||
val stateFlow = state.asStateFlow()
|
||||
|
||||
private val event = Channel<Event>()
|
||||
val eventFlow = event.receiveAsFlow()
|
||||
|
||||
fun onContinue() {
|
||||
state.update { it.copy(displayName = it.displayName.trim()) }
|
||||
|
||||
val displayName = state.value.displayName
|
||||
|
||||
when {
|
||||
displayName.isEmpty() -> { state.update { it.copy(error = R.string.activity_display_name_display_name_missing_error) } }
|
||||
displayName.length > NAME_PADDED_LENGTH -> { state.update { it.copy(error = R.string.activity_display_name_display_name_too_long_error) } }
|
||||
else -> {
|
||||
prefs.setProfileName(displayName)
|
||||
viewModelScope.launch { event.send(Event.DONE) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onChange(value: String) {
|
||||
state.update {
|
||||
it.copy(
|
||||
displayName = value,
|
||||
error = value.takeIf { it.length > NAME_PADDED_LENGTH }?.let { R.string.activity_display_name_display_name_too_long_error }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
@StringRes val title: Int = R.string.activity_display_name_title_2,
|
||||
@StringRes val description: Int = R.string.activity_display_name_explanation,
|
||||
@StringRes val error: Int? = null,
|
||||
val displayName: String = ""
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
object DONE: Event
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -17,11 +18,13 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -34,7 +37,6 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@ -45,6 +47,22 @@ import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun OutlineButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
modifier = modifier.size(108.dp, 34.dp),
|
||||
onClick = onClick,
|
||||
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
||||
shape = RoundedCornerShape(50), // = 50% percent
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = LocalExtraColors.current.prominentButtonColor,
|
||||
backgroundColor = MaterialTheme.colors.background
|
||||
)
|
||||
){
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemButton(
|
||||
text: String,
|
||||
|
@ -30,6 +30,7 @@ val LocalExtraColors = staticCompositionLocalOf<ExtraColors> { error("No Custom
|
||||
|
||||
data class ExtraColors(
|
||||
val settingsBackground: Color,
|
||||
val prominentButtonColor: Color
|
||||
)
|
||||
|
||||
/**
|
||||
@ -42,6 +43,7 @@ fun AppTheme(
|
||||
val extraColors = LocalContext.current.run {
|
||||
ExtraColors(
|
||||
settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
|
||||
prominentButtonColor = getColorFromTheme(R.attr.prominentButtonColor),
|
||||
)
|
||||
}
|
||||
|
||||
@ -98,6 +100,7 @@ val sessionTypography = Typography(
|
||||
)
|
||||
|
||||
val Typography.base get() = defaultStyle(14.sp)
|
||||
val Typography.baseBold get() = boldStyle(14.sp)
|
||||
val Typography.small get() = defaultStyle(12.sp)
|
||||
|
||||
val Typography.h7 get() = boldStyle(18.sp)
|
||||
|
@ -728,7 +728,7 @@
|
||||
<string name="activity_restore_explanation">Enter the recovery phrase that was given to you when you signed up to restore your account.</string>
|
||||
<string name="activity_restore_seed_edit_text_hint">Enter your recovery phrase</string>
|
||||
<string name="activity_display_name_title_2">Pick your display name</string>
|
||||
<string name="activity_display_name_explanation">This will be your name when you use Session. It can be your real name, an alias, or anything else you like.</string>
|
||||
<string name="activity_display_name_explanation">It can be your real name, an alias, or anything else you like — and you can change it any time.</string>
|
||||
<string name="activity_display_name_edit_text_hint">Enter a display name</string>
|
||||
<string name="activity_display_name_display_name_missing_error">Please pick a display name</string>
|
||||
<string name="activity_display_name_display_name_too_long_error">Please pick a shorter display name</string>
|
||||
|
@ -58,6 +58,7 @@
|
||||
<item name="prominentButtonColor">?colorAccent</item>
|
||||
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
|
||||
<item name="attachment_document_icon_large">@drawable/ic_document_large_dark</item>
|
||||
<item name="colorError">@color/destructive</item>
|
||||
</style>
|
||||
|
||||
<!-- This should be the default theme for the application. -->
|
||||
|
@ -11,7 +11,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath files('libs/gradle-witness.jar')
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||
@ -77,6 +77,6 @@ allprojects {
|
||||
project.ext {
|
||||
androidMinimumSdkVersion = 23
|
||||
androidTargetSdkVersion = 33
|
||||
androidCompileSdkVersion = 33
|
||||
androidCompileSdkVersion = 34
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@
|
||||
#Mon Jun 26 09:56:43 AEST 2023
|
||||
android.enableJetifier=true
|
||||
|
||||
gradlePluginVersion=7.3.1
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.unsafe.configuration-cache=true
|
||||
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Thu Dec 30 07:09:53 SAST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
Loading…
Reference in New Issue
Block a user