diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 9f22d7f1bd7..0a0be7e17b3 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -25,6 +25,7 @@ import com.nextcloud.talk.models.json.invitation.InvitationOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBanOverall +import com.nextcloud.talk.models.json.passwordResult.PasswordResultOverall import com.nextcloud.talk.models.json.profile.ProfileOverall import com.nextcloud.talk.models.json.reactions.ReactionsOverall import com.nextcloud.talk.models.json.status.StatusOverall @@ -577,4 +578,12 @@ interface NcApiCoroutines { @FormUrlEncoded @POST suspend fun reportRemoteWipeSuccess(@Url url: String, @Field("token") token: String): Response + + @FormUrlEncoded + @POST + suspend fun validatePassword( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("password") password: String + ): PasswordResultOverall } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index 0638284ba94..c5353bf3179 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -11,6 +11,8 @@ package com.nextcloud.talk.conversationcreation import android.annotation.SuppressLint import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri @@ -38,6 +40,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -89,6 +92,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector import coil.compose.AsyncImage import com.nextcloud.talk.R @@ -102,6 +106,7 @@ import com.nextcloud.talk.components.ColoredStatusBar import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.loadImage import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel +import com.nextcloud.talk.conversationcreation.viewmodel.ValidPasswordUiState import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.utils.CapabilitiesUtil @@ -625,95 +630,132 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM text = stringResource(id = R.string.nc_new_conversation_visibility), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp) - ) - ConversationOption( - icon = R.drawable.ic_avatar_link, - text = R.string.nc_guest_access_allow_title, - switch = { - Switch( - checked = isGuestsAllowed, - onCheckedChange = { - conversationCreationViewModel.isGuestsAllowed.value = it - } - ) - }, - conversationCreationViewModel = conversationCreationViewModel + modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) ) - if (isGuestsAllowed && !isPasswordSet) { - ConversationOption( - icon = R.drawable.baseline_lock_open_24, - text = R.string.nc_set_password, - conversationCreationViewModel = conversationCreationViewModel + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + ConversationOptions( + icon = R.drawable.ic_avatar_link, + text = R.string.nc_guest_access_allow_title, + switch = { + Switch( + checked = isGuestsAllowed, + onCheckedChange = { + conversationCreationViewModel.isGuestsAllowed.value = it + } + ) + }, + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = false, + copyPassword = false ) - } - if (isGuestsAllowed && isPasswordSet) { - ConversationOption( - icon = R.drawable.ic_lock_grey600_24px, - text = R.string.nc_change_password, - conversationCreationViewModel = conversationCreationViewModel - ) - } + if (isGuestsAllowed && !isPasswordSet) { + ConversationOptions( + icon = R.drawable.baseline_lock_open_24, + text = R.string.nc_set_password, + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = true, + copyPassword = false + ) + } - ConversationOption( - icon = R.drawable.baseline_format_list_bulleted_24, - text = R.string.nc_open_conversation_to_registered_users, - switch = { - Switch( - checked = isConversationAvailableForRegisteredUsers, - onCheckedChange = { - conversationCreationViewModel.isConversationAvailableForRegisteredUsers.value = it - } + if (isGuestsAllowed && isPasswordSet) { + ConversationOptions( + icon = R.drawable.ic_lock_grey600_24px, + text = R.string.nc_change_password, + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = true, + copyPassword = false ) - }, - conversationCreationViewModel = conversationCreationViewModel - ) + } + if (isGuestsAllowed && isPasswordSet) { + ConversationOptions( + icon = R.drawable.ic_content_copy, + text = R.string.nc_copy_password, + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = false, + copyPassword = true + ) + } - if (isConversationAvailableForRegisteredUsers) { - ConversationOption( - text = R.string.nc_open_to_guest_app_users, + ConversationOptions( + icon = R.drawable.baseline_format_list_bulleted_24, + text = R.string.nc_open_conversation_to_registered_users, switch = { Switch( - checked = isOpenForGuestAppUsers, + checked = isConversationAvailableForRegisteredUsers, onCheckedChange = { - conversationCreationViewModel.openForGuestAppUsers.value = it + conversationCreationViewModel.isConversationAvailableForRegisteredUsers.value = it } ) }, - conversationCreationViewModel = conversationCreationViewModel + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = false, + copyPassword = false ) + if (isConversationAvailableForRegisteredUsers) { + ConversationOptions( + text = R.string.nc_open_to_guest_app_users, + switch = { + Switch( + checked = isOpenForGuestAppUsers, + onCheckedChange = { + conversationCreationViewModel.openForGuestAppUsers.value = it + } + ) + }, + conversationCreationViewModel = conversationCreationViewModel, + isPasswordSetOrChange = false, + copyPassword = false + ) + } } } @Composable -fun ConversationOption( +@Suppress("LongMethod", "LongParameterList") +fun ConversationOptions( icon: Int? = null, text: Int, switch: @Composable (() -> Unit)? = null, - conversationCreationViewModel: ConversationCreationViewModel + conversationCreationViewModel: ConversationCreationViewModel, + isPasswordSetOrChange: Boolean, + copyPassword: Boolean ) { + val context = LocalContext.current var showPasswordDialog by rememberSaveable { mutableStateOf(false) } var showPasswordChangeDialog by rememberSaveable { mutableStateOf(false) } + val passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsStateWithLifecycle() Row( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .height(40.dp) + .padding(horizontal = 16.dp) .then( - if (!conversationCreationViewModel.isPasswordEnabled.value) { + if (!conversationCreationViewModel.isPasswordEnabled.value && isPasswordSetOrChange) { Modifier.clickable { showPasswordDialog = true } - } else if (conversationCreationViewModel.isPasswordEnabled.value) { + } else if (conversationCreationViewModel.isPasswordEnabled.value && isPasswordSetOrChange) { Modifier.clickable { showPasswordChangeDialog = true } + } else if (copyPassword) { + Modifier.clickable { + val clipboardManager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText( + context.resources.getString(R.string.nc_app_product_name), + conversationCreationViewModel.password.value + ) + clipboardManager.setPrimaryClip(clip) + } } else { Modifier } ), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { if (icon != null) { @@ -736,7 +778,8 @@ fun ConversationOption( if (showPasswordDialog) { ShowPasswordDialog( onDismiss = { showPasswordDialog = false }, - conversationCreationViewModel = conversationCreationViewModel + conversationCreationViewModel = conversationCreationViewModel, + passwordValidationState = passwordValidationState ) } if (showPasswordChangeDialog) { @@ -744,7 +787,8 @@ fun ConversationOption( onDismiss = { showPasswordChangeDialog = false }, - conversationCreationViewModel = conversationCreationViewModel + conversationCreationViewModel = conversationCreationViewModel, + passwordValidationState = passwordValidationState ) } } @@ -752,7 +796,11 @@ fun ConversationOption( @Suppress("LongMethod") @Composable -fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { +fun ShowChangePassword( + onDismiss: () -> Unit, + conversationCreationViewModel: ConversationCreationViewModel, + passwordValidationState: ValidPasswordUiState +) { var changedPassword by rememberSaveable { mutableStateOf("") } Dialog(onDismissRequest = { onDismiss() @@ -760,10 +808,11 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con Card( modifier = Modifier .fillMaxWidth() - .height(375.dp) + .wrapContentHeight() .padding(32.dp) .clip(RoundedCornerShape(16.dp)) .background(color = colorResource(id = R.color.appbar)) + .verticalScroll(rememberScrollState()) ) { Column( modifier = Modifier @@ -772,17 +821,23 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { + val validatePasswordUrl = conversationCreationViewModel + .currentUser.capabilities?.passwordCapability?.api?.validatePasswordApi Text(text = stringResource(id = R.string.nc_set_new_password), fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = changedPassword, onValueChange = { changedPassword = it + if (validatePasswordUrl != null) { + conversationCreationViewModel.validatePassword(validatePasswordUrl, it) + } }, label = { Text(text = stringResource(id = R.string.nc_password)) }, singleLine = true ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) + PasswordValidationMessage(passwordValidationState) Column( modifier = Modifier @@ -791,13 +846,18 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { + val securePassword = (passwordValidationState as? ValidPasswordUiState.Success)?.result?.passed + ?: false TextButton( onClick = { conversationCreationViewModel.updatePassword(changedPassword) conversationCreationViewModel.isPasswordEnabled.value = true + conversationCreationViewModel.resetPasswordViewState() onDismiss() }, - enabled = changedPassword.isNotEmpty() && changedPassword.isNotBlank(), + enabled = changedPassword.isNotEmpty() && + changedPassword.isNotBlank() && + securePassword, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { Text(text = stringResource(id = R.string.nc_change_password)) @@ -806,6 +866,7 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con TextButton( onClick = { conversationCreationViewModel.isPasswordEnabled.value = false + conversationCreationViewModel.resetPasswordViewState() onDismiss() }, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @@ -817,7 +878,11 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con } Spacer(modifier = Modifier.height(4.dp)) TextButton( - onClick = { onDismiss() }, + onClick = { + conversationCreationViewModel.resetPasswordViewState() + onDismiss() + }, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { Text(text = stringResource(id = R.string.nc_cancel)) @@ -829,27 +894,46 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con } @Composable -fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { +fun ShowPasswordDialog( + onDismiss: () -> Unit, + conversationCreationViewModel: ConversationCreationViewModel, + passwordValidationState: ValidPasswordUiState +) { var password by rememberSaveable { mutableStateOf("") } + val validatePasswordUrl = conversationCreationViewModel + .currentUser.capabilities?.passwordCapability?.api?.validatePasswordApi AlertDialog( containerColor = colorResource(id = R.color.dialog_background), onDismissRequest = onDismiss, title = { Text(text = stringResource(id = R.string.nc_set_password)) }, text = { - TextField( - value = password, - onValueChange = { - password = it - }, - label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) } - ) + Column { + TextField( + value = password, + onValueChange = { + password = it + if (validatePasswordUrl != null) { + conversationCreationViewModel.validatePassword(validatePasswordUrl, it) + } + }, + label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) } + ) + Spacer(modifier = Modifier.height(8.dp)) + PasswordValidationMessage(passwordValidationState) + } }, confirmButton = { + val securePassword = (passwordValidationState as? ValidPasswordUiState.Success)?.result?.passed + ?: false TextButton( onClick = { - if (password.isNotEmpty() && password.isNotBlank()) { + if (password.isNotEmpty() && + password.isNotBlank() && + securePassword + ) { conversationCreationViewModel.updatePassword(password) conversationCreationViewModel.isPasswordEnabled(true) + conversationCreationViewModel.resetPasswordViewState() onDismiss() } } @@ -858,13 +942,46 @@ fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: Con } }, dismissButton = { - TextButton(onClick = { onDismiss() }) { + TextButton(onClick = { + conversationCreationViewModel.resetPasswordViewState() + onDismiss() + }) { Text(text = stringResource(id = R.string.nc_cancel)) } } ) } +@Composable +fun PasswordValidationMessage(passwordValidationState: ValidPasswordUiState) { + when (passwordValidationState) { + is ValidPasswordUiState.Success -> Text( + text = passwordValidationState.result.reason + ?: stringResource(R.string.nc_password_secure), + color = if ((passwordValidationState).result.passed == false) { + colorResource( + id = R.color + .nc_darkRed + ) + } else { + colorResource(id = R.color.nc_darkGreen) + }, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + + is ValidPasswordUiState.Error -> { + Text( + text = passwordValidationState.message, + style = MaterialTheme.typography.bodySmall + ) + } + + else -> { + } + } +} + @Composable fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState() diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepository.kt index 7f3decfb7a4..de7e8fe8769 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepository.kt @@ -14,6 +14,7 @@ import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import com.nextcloud.talk.models.json.passwordResult.PasswordResultOverall import java.io.File interface ConversationCreationRepository { @@ -43,4 +44,5 @@ interface ConversationCreationRepository { color: String? ): RoomOverall suspend fun allowGuests(credentials: String?, url: String, token: String, allow: Boolean): GenericOverall + suspend fun validatePassword(credentials: String, url: String, password: String): PasswordResultOverall } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepositoryImpl.kt index a7cedd0cf9f..aa08b4fa02d 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/data/ConversationCreationRepositoryImpl.kt @@ -15,6 +15,7 @@ import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import com.nextcloud.talk.models.json.passwordResult.PasswordResultOverall import com.nextcloud.talk.utils.Mimetype import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -140,4 +141,13 @@ class ConversationCreationRepositoryImpl @Inject constructor(private val ncApiCo } return result } + + override suspend fun validatePassword(credentials: String, url: String, password: String): PasswordResultOverall { + val passwordOverall = ncApiCoroutines.validatePassword( + credentials, + url, + password + ) + return passwordOverall + } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt index a61e92b07b0..36101760102 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/viewmodel/ConversationCreationViewModel.kt @@ -18,6 +18,7 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.passwordResult.PasswordResult import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld @@ -34,6 +35,9 @@ class ConversationCreationViewModel @Inject constructor( val selectedParticipants: StateFlow> = _selectedParticipants private val roomViewState = MutableStateFlow(RoomUIState.None) + private val _validPasswordViewState = MutableStateFlow(ValidPasswordUiState.None) + val validPasswordViewState: StateFlow = _validPasswordViewState + private val _selectedImageUri = MutableStateFlow(null) val selectedImageUri: StateFlow = _selectedImageUri @@ -106,6 +110,10 @@ class ConversationCreationViewModel @Inject constructor( _conversationDescription.value = conversationDescription } + fun resetPasswordViewState() { + _validPasswordViewState.value = ValidPasswordUiState.None + } + fun updateConversationPreset(preset: String) { conversationPreset.value = preset when (preset) { @@ -124,6 +132,24 @@ class ConversationCreationViewModel @Inject constructor( } } + @Suppress("Detekt.TooGenericExceptionCaught", "LongMethod") + fun validatePassword(url: String, password: String) { + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + viewModelScope.launch { + try { + val passwordResult = repository.validatePassword( + credentials, + url, + password + ) + + _validPasswordViewState.value = ValidPasswordUiState.Success(passwordResult.ocs?.data!!) + } catch (exception: Exception) { + _validPasswordViewState.value = ValidPasswordUiState.Error(exception.message ?: "") + } + } + } + @Suppress("Detekt.TooGenericExceptionCaught", "LongMethod") fun createRoomAndAddParticipants( roomType: String, @@ -285,3 +311,9 @@ sealed class AddParticipantsUiState { data class Success(val participants: List?) : AddParticipantsUiState() data class Error(val message: String) : AddParticipantsUiState() } + +sealed class ValidPasswordUiState { + data object None : ValidPasswordUiState() + data class Success(val result: PasswordResult) : ValidPasswordUiState() + data class Error(val message: String) : ValidPasswordUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index bddda18c82e..8956357e064 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -7,19 +7,36 @@ package com.nextcloud.talk.conversationinfo import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager import android.content.Intent import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import androidx.activity.compose.setContent import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog as ComposeAlertDialog import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -54,7 +71,6 @@ import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogBanParticipantBinding -import com.nextcloud.talk.databinding.DialogPasswordBinding import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider import com.nextcloud.talk.extensions.getParcelableExtraProvider @@ -118,6 +134,10 @@ class ConversationInfoActivity : BaseActivity() { private var startGroupChat: Boolean = false + private var securePasswordViewState: ConversationInfoViewModel.SecurePasswordViewState + by mutableStateOf(ConversationInfoViewModel.SecurePasswordViewState.None) + private var showPasswordDialog by mutableStateOf(false) + private val workerData: Data? get() { val user = conversationUser ?: return null @@ -194,6 +214,8 @@ class ConversationInfoActivity : BaseActivity() { } } + viewModel.securePasswordViewState.observe(this) { securePasswordViewState = it } + setupCompose() } @@ -243,14 +265,57 @@ class ConversationInfoActivity : BaseActivity() { ColoredStatusBar() ConversationInfoScreen( state = uiState, - callbacks = buildCallbacks() + callbacks = buildCallbacks(onShowPasswordDialog = { showPasswordDialog = true }) ) + GuestAccessPasswordDialogHost() } } } + @Composable + private fun GuestAccessPasswordDialogHost() { + if (!showPasswordDialog) return + GuestAccessPasswordDialog( + validationState = securePasswordViewState, + onPasswordChanged = ::onGuestPasswordChanged, + onDismiss = ::dismissGuestPasswordDialog, + onSave = ::onGuestPasswordSave + ) + } + + private fun onGuestPasswordChanged(password: String) { + val user = conversationUser ?: return + val validatePasswordUrl = user.capabilities?.passwordCapability?.api?.validatePasswordApi ?: "" + viewModel.securePassword(credentials, validatePasswordUrl, password) + } + + private fun onGuestPasswordSave(password: String, copyAfterSave: Boolean) { + val user = conversationUser ?: return + if (copyAfterSave) { + copyPasswordToClipboard(password) + } + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + viewModel.setPassword( + user = user, + url = ApiUtils.getUrlForRoomPassword(apiVersion, user.baseUrl!!, conversationToken), + password = password + ) + dismissGuestPasswordDialog() + } + + private fun dismissGuestPasswordDialog() { + showPasswordDialog = false + viewModel.resetSecurePasswordViewState() + } + + private fun copyPasswordToClipboard(password: String) { + val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val label = resources.getString(R.string.nc_app_product_name) + clipboardManager.setPrimaryClip(ClipData.newPlainText(label, password)) + } + @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun buildCallbacks() = + private fun buildCallbacks(onShowPasswordDialog: () -> Unit) = ConversationInfoScreenCallbacks( onNavigateBack = { onBackPressedDispatcher.onBackPressed() }, onEditConversation = { @@ -287,7 +352,7 @@ class ConversationInfoActivity : BaseActivity() { password = "" ) } else { - showPasswordDialog(conversationToken) + onShowPasswordDialog() } }, onResendInvitationsClick = { @@ -613,31 +678,6 @@ class ConversationInfoActivity : BaseActivity() { } } - private fun showPasswordDialog(token: String) { - val user = conversationUser ?: return - val dialogPassword = DialogPasswordBinding.inflate(LayoutInflater.from(this)) - viewThemeUtils.platform.colorEditText(dialogPassword.password) - val builder = MaterialAlertDialogBuilder(this) - .setView(dialogPassword.root) - .setTitle(R.string.nc_guest_access_password_dialog_title) - .setPositiveButton(R.string.nc_ok) { _, _ -> - val apiVersion = - ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) - viewModel.setPassword( - user = user, - url = ApiUtils.getUrlForRoomPassword(apiVersion, user.baseUrl!!, token), - password = dialogPassword.password.text.toString() - ) - } - .setNegativeButton(R.string.nc_cancel, null) - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, builder) - val dialog = builder.show() - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - ) - } - private fun toggleModeratorStatus(apiVersion: Int, participant: Participant) { val user = conversationUser ?: return val subscriber = participantActionObserver() @@ -847,3 +887,90 @@ class ConversationInfoActivity : BaseActivity() { private const val PARTICIPANT_TYPE_USER: Int = 3 } } + +@Composable +@Suppress("LongMethod") +private fun GuestAccessPasswordDialog( + validationState: ConversationInfoViewModel.SecurePasswordViewState, + onPasswordChanged: (String) -> Unit, + onDismiss: () -> Unit, + onSave: (password: String, copyAfterSave: Boolean) -> Unit +) { + var password by rememberSaveable { mutableStateOf("") } + val secureText = stringResource(R.string.nc_password_secure) + val warningMessage = passwordWarningMessage(validationState, secureText) + val isPasswordValid = password.isNotBlank() && warningMessage == secureText + + ComposeAlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = password, + onValueChange = { + password = it + onPasswordChanged(it) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { + Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) + }, + supportingText = { + warningMessage?.let { + Text( + text = it, + color = if (!isPasswordValid) { + colorResource(R.color.nc_darkRed) + } else { + colorResource(R.color.nc_darkGreen) + } + ) + } + } + ) + } + }, + confirmButton = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextButton( + onClick = { onSave(password, true) }, + enabled = isPasswordValid + ) { + Text(text = stringResource(R.string.nc_copy_password)) + } + TextButton( + onClick = { onSave(password, false) }, + enabled = isPasswordValid + ) { + Text(text = stringResource(R.string.save)) + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.nc_cancel)) + } + } + ) +} + +@Composable +private fun passwordWarningMessage( + validationState: ConversationInfoViewModel.SecurePasswordViewState, + secureText: String +): String? = + when (validationState) { + is ConversationInfoViewModel.SecurePasswordViewState.Success -> { + validationState.result.passed?.let { passed -> + if (passed) secureText else validationState.result.reason + } + } + + is ConversationInfoViewModel.SecurePasswordViewState.Error -> { + stringResource(R.string.nc_common_error_sorry) + } + + ConversationInfoViewModel.SecurePasswordViewState.None -> "" + } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index dae28789b46..9046d6fbdd6 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -16,6 +16,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationcreation.data.ConversationCreationRepository import com.nextcloud.talk.conversationinfo.ConversationInfoUiEvent import com.nextcloud.talk.conversationinfo.ConversationInfoUiState import com.nextcloud.talk.conversationinfo.CreateRoomRequest @@ -36,6 +37,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS import com.nextcloud.talk.models.json.participants.ParticipantsOverall import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.passwordResult.PasswordResult import com.nextcloud.talk.models.json.profile.Profile import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult @@ -74,7 +76,8 @@ import javax.inject.Inject class ConversationInfoViewModel @Inject constructor( private val chatNetworkDataSource: ChatNetworkDataSource, private val conversationsRepository: ConversationsRepository, - private val ncApi: NcApi + private val ncApi: NcApi, + private val conversationCreationRepository: ConversationCreationRepository ) : ViewModel() { object LifeCycleObserver : DefaultLifecycleObserver { enum class LifeCycleFlag { @@ -168,6 +171,54 @@ class ConversationInfoViewModel @Inject constructor( } return uiItems } + + private val _passwordViewState = MutableLiveData(PasswordUiState.None) + val passwordViewState: LiveData + get() = _passwordViewState + + private val _securePasswordViewState = MutableLiveData(SecurePasswordViewState.None) + val securePasswordViewState: LiveData = _securePasswordViewState + + private val _clearChatHistoryViewState: MutableLiveData = + MutableLiveData(ClearChatHistoryViewState.None) + val clearChatHistoryViewState: LiveData + get() = _clearChatHistoryViewState + + private val _getConversationReadOnlyState: MutableLiveData = + MutableLiveData(SetConversationReadOnlyViewState.None) + val getConversationReadOnlyState: LiveData + get() = _getConversationReadOnlyState + + @Suppress("PropertyName") + private val _markConversationAsImportantResult = + MutableLiveData(MarkConversationAsImportantViewState.None) + val markAsImportantResult: LiveData + get() = _markConversationAsImportantResult + + @Suppress("PropertyName") + private val _markConversationAsUnimportantResult = + MutableLiveData(MarkConversationAsUnimportantViewState.None) + val markAsUnimportantResult: LiveData + get() = _markConversationAsUnimportantResult + + object GetProfileErrorState : ViewState + class GetProfileSuccessState(val profile: Profile) : ViewState + private val _getProfileViewState = MutableLiveData() + val getProfileViewState: LiveData + get() = _getProfileViewState + + @Suppress("PropertyName") + private val _markConversationAsSensitiveResult = + MutableLiveData(MarkConversationAsSensitiveViewState.None) + val markAsSensitiveResult: LiveData + get() = _markConversationAsSensitiveResult + + @Suppress("PropertyName") + private val _markConversationAsInsensitiveResult = + MutableLiveData(MarkConversationAsInsensitiveViewState.None) + val markAsInsensitiveResult: LiveData + get() = _markConversationAsInsensitiveResult + fun getRoom(user: User, token: String) { currentUser = user currentToken = token @@ -181,6 +232,10 @@ class ConversationInfoViewModel @Inject constructor( ?.subscribe(GetRoomObserver()) } + fun resetSecurePasswordViewState() { + _securePasswordViewState.value = SecurePasswordViewState.None + } + @Suppress("Detekt.TooGenericExceptionCaught") fun createRoomFromOneToOne( user: User, @@ -224,6 +279,24 @@ class ConversationInfoViewModel @Inject constructor( } } } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun securePassword(credentials: String, url: String, password: String) { + viewModelScope.launch { + try { + val passwordResult = conversationCreationRepository.validatePassword( + credentials, + url, + password + ) + + _securePasswordViewState.value = SecurePasswordViewState.Success(passwordResult.ocs?.data!!) + } catch (exception: Exception) { + _securePasswordViewState.value = SecurePasswordViewState.Error(exception.message ?: "") + } + } + } + private fun convertAutocompleteUserToParticipant(autocompleteUsers: List): Participants { val participants = Participants() autocompleteUsers.forEach { autocompleteUser -> @@ -942,4 +1015,58 @@ class ConversationInfoViewModel @Inject constructor( return DisplayUtils.ellipsize(conversationName, MAX_ROOM_NAME_LENGTH) } } + + sealed class ClearChatHistoryViewState { + data object None : ClearChatHistoryViewState() + data object Success : ClearChatHistoryViewState() + data class Error(val exception: Exception) : ClearChatHistoryViewState() + } + + sealed class MarkConversationAsSensitiveViewState { + data object None : MarkConversationAsSensitiveViewState() + data class Success(val statusCode: Int) : MarkConversationAsSensitiveViewState() + data class Error(val exception: Exception) : MarkConversationAsSensitiveViewState() + } + + sealed class MarkConversationAsInsensitiveViewState { + data object None : MarkConversationAsInsensitiveViewState() + data class Success(val statusCode: Int) : MarkConversationAsInsensitiveViewState() + data class Error(val exception: Exception) : MarkConversationAsInsensitiveViewState() + } + + sealed class SetConversationReadOnlyViewState { + data object None : SetConversationReadOnlyViewState() + data object Success : SetConversationReadOnlyViewState() + data class Error(val exception: Exception) : SetConversationReadOnlyViewState() + } + + sealed class AllowGuestsUIState { + data object None : AllowGuestsUIState() + data class Success(val allow: Boolean) : AllowGuestsUIState() + data class Error(val exception: Exception) : AllowGuestsUIState() + } + + sealed class PasswordUiState { + data object None : PasswordUiState() + data object Success : PasswordUiState() + data class Error(val exception: Exception) : PasswordUiState() + } + + sealed class MarkConversationAsImportantViewState { + data object None : MarkConversationAsImportantViewState() + data class Success(val statusCode: Int) : MarkConversationAsImportantViewState() + data class Error(val exception: Exception) : MarkConversationAsImportantViewState() + } + + sealed class MarkConversationAsUnimportantViewState { + data object None : MarkConversationAsUnimportantViewState() + data class Success(val statusCode: Int) : MarkConversationAsUnimportantViewState() + data class Error(val exception: Exception) : MarkConversationAsUnimportantViewState() + } + + sealed class SecurePasswordViewState { + data object None : SecurePasswordViewState() + data class Success(val result: PasswordResult) : SecurePasswordViewState() + data class Error(val message: String) : SecurePasswordViewState() + } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt index c0cad49e802..16245f465f0 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt @@ -29,8 +29,10 @@ data class Capabilities( @JsonField(name = ["provisioning_api"]) var provisioningCapability: ProvisioningCapability?, @JsonField(name = ["user_status"]) - var userStatusCapability: UserStatusCapability? + var userStatusCapability: UserStatusCapability?, + @JsonField(name = ["password_policy"]) + var passwordCapability: PasswordPolicy? ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null, null, null, null, null, null) + constructor() : this(null, null, null, null, null, null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordAccount.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordAccount.kt new file mode 100644 index 00000000000..27d01245db3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordAccount.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordAccount( + @JsonField(name = ["minLength"]) + var minLength: Int, + @JsonField(name = ["enforceHaveIBeenPwned"]) + var enforceHaveIBeenPwned: Boolean, + @JsonField(name = ["enforceNonCommonPassword"]) + var enforceNonCommonPassword: Boolean, + @JsonField(name = ["enforceNumericCharacters"]) + var enforceNumericCharacters: Boolean, + @JsonField(name = ["enforceSpecialCharacters"]) + var enforceSpecialCharacters: Boolean, + @JsonField(name = ["enforceUpperLowerCase"]) + var enforceUpperLowerCase: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, false, false, false, false, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordApi.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordApi.kt new file mode 100644 index 00000000000..9ba832ac210 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordApi.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordApi( + @JsonField(name = ["generate"]) + var generatePasswordApi: String?, + @JsonField(name = ["validate"]) + var validatePasswordApi: String? + +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordCapability.kt new file mode 100644 index 00000000000..aabc10a719a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordCapability.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordCapability( + @JsonField(name = ["password_policy"]) + var passwordPolicy: PasswordPolicy? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicies.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicies.kt new file mode 100644 index 00000000000..98b47cc4954 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicies.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordPolicies( + @JsonField(name = ["account"]) + var api: PasswordAccount? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicy.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicy.kt new file mode 100644 index 00000000000..8bbc86fd144 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicy.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordPolicy( + @JsonField(name = ["api"]) + var api: PasswordApi?, + @JsonField(name = ["policies"]) + var policies: PasswordPolicies?, + @JsonField(name = ["minLength"]) + var minLength: Int, + @JsonField(name = ["enforceNonCommonPassword"]) + var enforceNonCommonPassword: Boolean, + @JsonField(name = ["enforceNumericCharacters"]) + var enforceNumericCharacters: Boolean, + @JsonField(name = ["enforceSpecialCharacters"]) + var enforceSpecialCharacters: Boolean, + @JsonField(name = ["enforceUpperLowerCase"]) + var enforceUpperLowerCase: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, 0, false, false, false, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt index 0cd08fef1ad..0dfcede1eea 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt @@ -1,4 +1,3 @@ - /* * Nextcloud Talk - Android Client * diff --git a/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResult.kt b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResult.kt new file mode 100644 index 00000000000..cc45672141f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResult.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.passwordResult + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordResult( + @JsonField(name = ["passed"]) + var passed: Boolean?, + @JsonField(name = ["reason"]) + var reason: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOCS.kt new file mode 100644 index 00000000000..d967656a243 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.passwordResult + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordResultOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: PasswordResult? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOverall.kt new file mode 100644 index 00000000000..5f202864c99 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.passwordResult + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordResultOverall( + @JsonField(name = ["ocs"]) + var ocs: PasswordResultOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml deleted file mode 100644 index a4647154ae5..00000000000 --- a/app/src/main/res/layout/dialog_password.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbe92cd498b..b641f2b44c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1086,4 +1086,6 @@ How to translate with transifex: Turn on background blur Turn off background blur Account not found + Password is secure + Copy password