Приложение
This commit is contained in:
208
client-mobile/profiles/presentation/EditProfileScreen.kt
Normal file
208
client-mobile/profiles/presentation/EditProfileScreen.kt
Normal file
@@ -0,0 +1,208 @@
|
||||
package profiles.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CameraAlt
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil.compose.AsyncImage
|
||||
import core.presentation.components.AppAvatar
|
||||
import core.presentation.theme.SoftSquareShape
|
||||
import ru.knot.messager.R
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import core.utils.ImageCropper
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditProfileScreen(
|
||||
currentDisplayName: String?,
|
||||
currentUsername: String,
|
||||
currentBio: String?,
|
||||
currentAvatarUrl: String?,
|
||||
onSave: (displayName: String, bio: String, avatarUri: Uri?) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var displayName by remember { mutableStateOf(currentDisplayName ?: "") }
|
||||
var bio by remember { mutableStateOf(currentBio ?: "") }
|
||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
// Лаунчер для результата кропа
|
||||
val cropLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
selectedImageUri = UCrop.getOutput(result.data!!)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Лаунчер для выбора фото из галереи
|
||||
val photoPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri ->
|
||||
if (uri != null) {
|
||||
// После выбора фото запускаем UCrop
|
||||
val destinationUri = Uri.fromFile(File(context.cacheDir, "${UUID.randomUUID()}.jpg"))
|
||||
val cropIntent = ImageCropper.getCropIntent(context, uri, destinationUri).getIntent(context)
|
||||
cropLauncher.launch(cropIntent)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.edit_profile)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { onSave(displayName, bio, selectedImageUri) }) {
|
||||
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save), tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Смена фото
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.clip(SoftSquareShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable {
|
||||
photoPickerLauncher.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (selectedImageUri != null) {
|
||||
AsyncImage(
|
||||
model = selectedImageUri,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
AppAvatar(
|
||||
url = currentAvatarUrl,
|
||||
name = currentUsername,
|
||||
size = 120.dp
|
||||
)
|
||||
}
|
||||
|
||||
// Overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CameraAlt,
|
||||
contentDescription = stringResource(R.string.change_photo),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
photoPickerLauncher.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.change_photo))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Поля ввода
|
||||
OutlinedTextField(
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
label = { Text(stringResource(R.string.display_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = SoftSquareShape
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = "@$currentUsername",
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.username_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = false, // Обычно username меняется отдельно или вообще не меняется
|
||||
shape = SoftSquareShape,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outlineVariant,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = bio,
|
||||
onValueChange = { bio = it },
|
||||
label = { Text(stringResource(R.string.bio)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 100.dp),
|
||||
shape = SoftSquareShape,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = { onSave(displayName, bio, selectedImageUri) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = SoftSquareShape
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
304
client-mobile/profiles/presentation/ProfileScreen.kt
Normal file
304
client-mobile/profiles/presentation/ProfileScreen.kt
Normal file
@@ -0,0 +1,304 @@
|
||||
package profiles.presentation
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import core.presentation.components.AppAvatar
|
||||
import core.presentation.theme.SoftSquareShape
|
||||
import kotlinx.coroutines.launch
|
||||
import profiles.presentation.viewmodel.ProfileViewModel
|
||||
import ru.knot.messager.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
profileId: String? = null, // null for own profile
|
||||
viewModel: ProfileViewModel = hiltViewModel(),
|
||||
onEditProfile: () -> Unit = {},
|
||||
onSendMessage: (String) -> Unit = {},
|
||||
onCall: (String) -> Unit = {},
|
||||
onBack: () -> Unit = {}
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(profileId) {
|
||||
viewModel.loadProfile(profileId)
|
||||
}
|
||||
|
||||
val profile = state.profile
|
||||
val isOwnProfile = profileId == null
|
||||
|
||||
val mediaTabs = listOf(
|
||||
stringResource(R.string.media),
|
||||
"GIF",
|
||||
"Файлы",
|
||||
"Ссылки"
|
||||
)
|
||||
|
||||
val pagerState = rememberPagerState(pageCount = { mediaTabs.size })
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (state.isLoading) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(if (isOwnProfile) stringResource(R.string.profile) else stringResource(R.string.profile)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isOwnProfile) {
|
||||
IconButton(onClick = onEditProfile) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit_profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
ProfileHeader(
|
||||
username = profile?.username ?: "",
|
||||
displayName = profile?.displayName,
|
||||
avatarUrl = profile?.avatarUrl,
|
||||
bio = profile?.bio,
|
||||
isOwnProfile = isOwnProfile,
|
||||
isCallsEnabled = true,
|
||||
onSendMessage = { profile?.id?.let { id -> onSendMessage(id) } },
|
||||
onCall = { profile?.id?.let { id -> onCall(id) } }
|
||||
)
|
||||
}
|
||||
|
||||
stickyHeader {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
edgePadding = 16.dp,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
divider = {}
|
||||
) {
|
||||
mediaTabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = { Text(title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Using item with fillParentMaxSize for Pager inside LazyColumn
|
||||
// is tricky, but here we render content directly in LazyColumn
|
||||
// to maintain scrolling, or use a fixed height.
|
||||
// For gestures, we implement HorizontalPager for the content:
|
||||
}
|
||||
|
||||
// Alternative: Use HorizontalPager for the whole content area below header
|
||||
// But to keep header scrolling, we need nested scroll or a different approach.
|
||||
// Let's implement HorizontalPager for media content items specifically.
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileHeader(
|
||||
username: String,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
bio: String?,
|
||||
isOwnProfile: Boolean,
|
||||
isCallsEnabled: Boolean,
|
||||
onSendMessage: () -> Unit,
|
||||
onCall: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
AppAvatar(
|
||||
url = avatarUrl,
|
||||
name = username,
|
||||
size = 120.dp
|
||||
)
|
||||
if (isOwnProfile) {
|
||||
FilledIconButton(
|
||||
onClick = { /* Выбор фото */ },
|
||||
modifier = Modifier.size(32.dp).offset(x = 4.dp, y = 4.dp),
|
||||
shape = CircleShape,
|
||||
colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(Icons.Default.CameraAlt, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(text = displayName ?: username, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(text = "@$username", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
if (!isOwnProfile) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
ActionCircleButton(Icons.Default.Message, stringResource(R.string.message), onSendMessage)
|
||||
if (isCallsEnabled) {
|
||||
ActionCircleButton(Icons.Default.Call, stringResource(R.string.call), onCall)
|
||||
}
|
||||
ActionCircleButton(Icons.Default.Notifications, stringResource(R.string.notifications), {})
|
||||
}
|
||||
}
|
||||
|
||||
if (bio != null) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.bio), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||
Text(text = bio, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderMediaContent(tabIndex: Int) {
|
||||
// В реальном приложении данные приходят из ViewModel
|
||||
// Группировка по месяцам
|
||||
val months = listOf("Октябрь 2023", "Сентябрь 2023")
|
||||
|
||||
months.forEach { month ->
|
||||
item {
|
||||
Text(
|
||||
text = month,
|
||||
modifier = Modifier.padding(16.dp, 8.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
when (tabIndex) {
|
||||
0 -> renderMediaGrid() // Медиа (сетка 3x3)
|
||||
1 -> renderGifs() // GIF
|
||||
2 -> renderFiles() // Файлы
|
||||
3 -> renderLinks() // Ссылки
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные функции для рендеринга контента...
|
||||
fun LazyListScope.renderMediaGrid() {
|
||||
item {
|
||||
// Упрощенная сетка внутри LazyColumn
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
repeat(3) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.padding(2.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Gray.copy(alpha = 0.2f))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderFiles() {
|
||||
items(3) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Document.pdf") },
|
||||
supportingContent = { Text("2.4 MB • 12.10.23") },
|
||||
leadingContent = { Icon(Icons.Default.Description, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderLinks() {
|
||||
items(2) {
|
||||
ListItem(
|
||||
headlineContent = { Text("https://github.com/forkmessager") },
|
||||
supportingContent = { Text("GitHub - ForkMessager Project") },
|
||||
leadingContent = { Icon(Icons.Default.Link, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderGifs() {
|
||||
item {
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
repeat(2) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(120.dp)
|
||||
.padding(2.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Gray.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("GIF", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionCircleButton(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
FilledIconButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.size(56.dp),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(containerColor = color.copy(alpha = 0.1f))
|
||||
) {
|
||||
Icon(icon, contentDescription = label, tint = color)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(text = label, style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package profiles.presentation.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import profiles.data.remote.dto.ProfileDto
|
||||
import profiles.domain.repository.ProfileRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
data class ProfileState(
|
||||
val profile: ProfileDto? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSaving: Boolean = false
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor(
|
||||
private val repository: ProfileRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(ProfileState())
|
||||
val state: StateFlow<ProfileState> = _state.asStateFlow()
|
||||
|
||||
fun loadProfile(id: String? = null) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isLoading = true)
|
||||
val result = if (id == null) {
|
||||
repository.getMyProfile()
|
||||
} else {
|
||||
repository.getProfile(id)
|
||||
}
|
||||
|
||||
result.onSuccess { profile ->
|
||||
_state.value = _state.value.copy(profile = profile, isLoading = false)
|
||||
}.onFailure { error ->
|
||||
_state.value = _state.value.copy(error = error.message, isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateProfile(displayName: String?, bio: String?, avatarUri: Uri? = null) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isSaving = true)
|
||||
|
||||
// 1. Обновляем аватар, если он выбран
|
||||
avatarUri?.let {
|
||||
repository.uploadAvatar(it)
|
||||
}
|
||||
|
||||
// 2. Обновляем текстовые данные
|
||||
repository.updateProfile(displayName, bio)
|
||||
.onSuccess { updatedProfile ->
|
||||
_state.value = _state.value.copy(profile = updatedProfile, isSaving = false)
|
||||
}
|
||||
.onFailure { error ->
|
||||
_state.value = _state.value.copy(error = error.message, isSaving = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAvatar() {
|
||||
viewModelScope.launch {
|
||||
repository.removeAvatar().onSuccess { updatedProfile ->
|
||||
_state.value = _state.value.copy(profile = updatedProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user