From 87949c1251e8ef4210c0d024907a1fc9b4209ad0 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 4 Mar 2026 18:09:20 +0100 Subject: [PATCH 1/5] add model Signed-off-by: sowjanyakch --- .../models/json/capabilities/Capabilities.kt | 6 ++-- .../json/capabilities/PasswordAccount.kt | 33 +++++++++++++++++ .../models/json/capabilities/PasswordApi.kt | 26 ++++++++++++++ .../json/capabilities/PasswordCapability.kt | 23 ++++++++++++ .../json/capabilities/PasswordPolicies.kt | 23 ++++++++++++ .../json/capabilities/PasswordPolicy.kt | 35 +++++++++++++++++++ .../capabilities/ProvisioningCapability.kt | 2 -- 7 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordAccount.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordApi.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicies.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/PasswordPolicy.kt 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..df468ec9ed0 --- /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 Your Name + * 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..01a4118b945 --- /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 Your Name + * 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..9c7732c94b8 --- /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 Your Name + * 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..f656554b278 --- /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 Your Name + * 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..477d0b5aff3 --- /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 Your Name + * 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..12c29d11305 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 @@ -12,11 +12,9 @@ import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable @Parcelize @JsonObject -@Serializable data class ProvisioningCapability( @JsonField(name = ["AccountPropertyScopesVersion"]) var accountPropertyScopesVersion: Int? From 8569d7803a521d5cdce0588ee70e6246f8eaa7fc Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 11 Mar 2026 10:35:24 +0100 Subject: [PATCH 2/5] add password logic Signed-off-by: sowjanyakch --- .../com/nextcloud/talk/api/NcApiCoroutines.kt | 9 +++ .../ConversationCreationActivity.kt | 76 ++++++++++++++++--- .../ConversationCreationRepository.kt | 2 + .../ConversationCreationRepositoryImpl.kt | 10 +++ .../ConversationCreationViewModel.kt | 28 +++++++ .../json/passwordResult/PasswordResult.kt | 25 ++++++ .../json/passwordResult/PasswordResultOCS.kt | 26 +++++++ .../passwordResult/PasswordResultOverall.kt | 23 ++++++ 8 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResult.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResultOverall.kt 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 94cfdf4b728..aad151151f1 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -19,6 +19,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.status.StatusOverall import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall @@ -374,4 +375,12 @@ interface NcApiCoroutines { @GET suspend fun getScheduledMessage(@Header("Authorization") authorization: String, @Url url: String): ChatOverall + + @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 64307e6f8f2..a2ecc426daa 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -591,6 +591,7 @@ fun ConversationOptions( @Composable fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { var changedPassword by rememberSaveable { mutableStateOf("") } + val passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsState() Dialog(onDismissRequest = { onDismiss() }) { @@ -609,17 +610,42 @@ 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)) + when (passwordValidationState) { + is ValidPasswordUiState.Success -> Text( + text = (passwordValidationState as ValidPasswordUiState.Success).result.reason!!, + color = if ((passwordValidationState as ValidPasswordUiState.Success).result.passed == false) { + colorResource( + id = R.color + .nc_darkRed + ) + } else { + colorResource(id = R.color.nc_darkGreen) + }, + modifier = Modifier.fillMaxWidth() + ) + + is ValidPasswordUiState.Error -> { + Text(text = (passwordValidationState as ValidPasswordUiState.Error).message) + } + + else -> { + } + } Column( modifier = Modifier @@ -634,7 +660,9 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con conversationCreationViewModel.isPasswordEnabled.value = true onDismiss() }, - enabled = changedPassword.isNotEmpty() && changedPassword.isNotBlank(), + enabled = changedPassword.isNotEmpty() && + changedPassword.isNotBlank() && + (passwordValidationState as ValidPasswordUiState.Success).result.passed == true, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { Text(text = stringResource(id = R.string.nc_change_password)) @@ -668,18 +696,48 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con @Composable fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { var password by rememberSaveable { mutableStateOf("") } + val passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsState() + 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)) } - ) + Row { + 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)) + + when (passwordValidationState) { + is ValidPasswordUiState.Success -> Text( + text = (passwordValidationState as ValidPasswordUiState.Success).result.reason!!, + color = if ((passwordValidationState as ValidPasswordUiState.Success).result.passed == false) { + colorResource( + id = R.color + .nc_darkRed + ) + } else { + colorResource(id = R.color.nc_darkGreen) + }, + modifier = Modifier.fillMaxWidth() + ) + + is ValidPasswordUiState.Error -> { + Text(text = (passwordValidationState as ValidPasswordUiState.Error).message) + } + + else -> { + } + } + } }, confirmButton = { TextButton( diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt index e065f7aa34e..e537cb60590 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt @@ -13,6 +13,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 { @@ -35,4 +36,5 @@ interface ConversationCreationRepository { roomToken: String ): ConversationModel 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/ConversationCreationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt index d864ebd4364..a88b6f2c0a0 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.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 com.nextcloud.talk.utils.Mimetype import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -121,4 +122,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/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index 35be3ecec1c..e162396ac72 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -18,6 +18,7 @@ import com.nextcloud.talk.models.RetrofitBucket import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.models.json.passwordResult.PasswordResult import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl.Companion.STATUS_CODE_OK import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils.getRetrofitBucketForAddParticipant @@ -36,6 +37,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 @@ -80,6 +84,24 @@ class ConversationCreationViewModel @Inject constructor( _conversationDescription.value = conversationDescription } + @Suppress("Detekt.TooGenericExceptionCaught") + 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") fun createRoomAndAddParticipants( roomType: String, @@ -257,3 +279,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/models/json/passwordResult/PasswordResult.kt b/app/src/main/java/com/nextcloud/talk/models/json/passwordResult/PasswordResult.kt new file mode 100644 index 00000000000..d73222645f8 --- /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 Your Name + * 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..8742cca1035 --- /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 Your Name + * 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..45f1904ead6 --- /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 Your Name + * 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) +} From af95b986467922da3595b7edfd0145a0bcb1b5d9 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 11 Mar 2026 12:32:25 +0100 Subject: [PATCH 3/5] handle states of password states Signed-off-by: sowjanyakch --- .../ConversationCreationActivity.kt | 130 ++++++++++-------- .../ConversationCreationViewModel.kt | 4 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 78 insertions(+), 57 deletions(-) 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 a2ecc426daa..f12d10f0bc8 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -38,6 +38,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 @@ -81,6 +82,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 @@ -533,6 +535,7 @@ fun ConversationOptions( ) { var showPasswordDialog by rememberSaveable { mutableStateOf(false) } var showPasswordChangeDialog by rememberSaveable { mutableStateOf(false) } + val passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsStateWithLifecycle() Row( modifier = Modifier .fillMaxWidth() @@ -573,7 +576,8 @@ fun ConversationOptions( if (showPasswordDialog) { ShowPasswordDialog( onDismiss = { showPasswordDialog = false }, - conversationCreationViewModel = conversationCreationViewModel + conversationCreationViewModel = conversationCreationViewModel, + passwordValidationState = passwordValidationState ) } if (showPasswordChangeDialog) { @@ -581,7 +585,8 @@ fun ConversationOptions( onDismiss = { showPasswordChangeDialog = false }, - conversationCreationViewModel = conversationCreationViewModel + conversationCreationViewModel = conversationCreationViewModel, + passwordValidationState = passwordValidationState ) } } @@ -589,19 +594,23 @@ fun ConversationOptions( @Suppress("LongMethod") @Composable -fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { +fun ShowChangePassword( + onDismiss: () -> Unit, + conversationCreationViewModel: ConversationCreationViewModel, + passwordValidationState: ValidPasswordUiState +) { var changedPassword by rememberSaveable { mutableStateOf("") } - val passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsState() Dialog(onDismissRequest = { onDismiss() }) { 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 @@ -610,7 +619,8 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - val validatePasswordUrl = conversationCreationViewModel.currentUser.capabilities?.passwordCapability?.api?.validatePasswordApi + 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( @@ -625,27 +635,7 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con singleLine = true ) Spacer(modifier = Modifier.height(8.dp)) - when (passwordValidationState) { - is ValidPasswordUiState.Success -> Text( - text = (passwordValidationState as ValidPasswordUiState.Success).result.reason!!, - color = if ((passwordValidationState as ValidPasswordUiState.Success).result.passed == false) { - colorResource( - id = R.color - .nc_darkRed - ) - } else { - colorResource(id = R.color.nc_darkGreen) - }, - modifier = Modifier.fillMaxWidth() - ) - - is ValidPasswordUiState.Error -> { - Text(text = (passwordValidationState as ValidPasswordUiState.Error).message) - } - - else -> { - } - } + PasswordValidationMessage(passwordValidationState) Column( modifier = Modifier @@ -654,15 +644,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() && - (passwordValidationState as ValidPasswordUiState.Success).result.passed == true, + securePassword, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { Text(text = stringResource(id = R.string.nc_change_password)) @@ -671,6 +664,7 @@ fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: Con TextButton( onClick = { conversationCreationViewModel.isPasswordEnabled.value = false + conversationCreationViewModel.resetPasswordViewState() onDismiss() }, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @@ -682,7 +676,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)) @@ -694,16 +692,20 @@ 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 passwordValidationState by conversationCreationViewModel.validPasswordViewState.collectAsState() - val validatePasswordUrl = conversationCreationViewModel.currentUser.capabilities?.passwordCapability?.api?.validatePasswordApi + 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 = { - Row { + Column { TextField( value = password, onValueChange = { @@ -715,36 +717,21 @@ fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: Con label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) } ) Spacer(modifier = Modifier.height(8.dp)) - - when (passwordValidationState) { - is ValidPasswordUiState.Success -> Text( - text = (passwordValidationState as ValidPasswordUiState.Success).result.reason!!, - color = if ((passwordValidationState as ValidPasswordUiState.Success).result.passed == false) { - colorResource( - id = R.color - .nc_darkRed - ) - } else { - colorResource(id = R.color.nc_darkGreen) - }, - modifier = Modifier.fillMaxWidth() - ) - - is ValidPasswordUiState.Error -> { - Text(text = (passwordValidationState as ValidPasswordUiState.Error).message) - } - - else -> { - } - } + 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() } } ) { @@ -752,13 +739,42 @@ 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 as ValidPasswordUiState.Success).result.passed == false) { + colorResource( + id = R.color + .nc_darkRed + ) + } else { + colorResource(id = R.color.nc_darkGreen) + }, + modifier = Modifier.fillMaxWidth() + ) + + is ValidPasswordUiState.Error -> { + Text(text = passwordValidationState.message) + } + + 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/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index e162396ac72..6b9ce360f82 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -84,6 +84,10 @@ class ConversationCreationViewModel @Inject constructor( _conversationDescription.value = conversationDescription } + fun resetPasswordViewState() { + _validPasswordViewState.value = ValidPasswordUiState.None + } + @Suppress("Detekt.TooGenericExceptionCaught") fun validatePassword(url: String, password: String) { val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c6dd2cc04d..3dc61ad5b4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -959,4 +959,5 @@ How to translate with transifex: No connection to server - Scheduled messages could not be loaded Show app switcher Nextcloud app suggestions in account chooser dialog + Password is secure From 2280c23307ffd81a32a1b954959b2e1b081a1ebc Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 11 Mar 2026 14:16:46 +0100 Subject: [PATCH 4/5] copy password Signed-off-by: sowjanyakch --- .../ConversationCreationActivity.kt | 136 +++++---- .../conversationinfo/GuestAccessHelper.kt | 271 ++++++++++++------ .../viewmodel/ConversationInfoViewModel.kt | 35 ++- app/src/main/res/layout/dialog_password.xml | 12 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 326 insertions(+), 129 deletions(-) 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 f12d10f0bc8..a99cb2eae08 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -12,6 +12,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 @@ -466,63 +468,84 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM fontSize = 14.sp, modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.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 - ) - if (isGuestsAllowed && !isPasswordSet) { + Column { ConversationOptions( - icon = R.drawable.baseline_lock_open_24, - text = R.string.nc_set_password, - conversationCreationViewModel = conversationCreationViewModel + 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) { - ConversationOptions( - 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 + ) + } - ConversationOptions( - 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) { ConversationOptions( - text = R.string.nc_open_to_guest_app_users, + 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 + ) + } } } @@ -531,8 +554,11 @@ 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() @@ -541,19 +567,29 @@ fun ConversationOptions( .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 8.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.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) { @@ -755,7 +791,7 @@ fun PasswordValidationMessage(passwordValidationState: ValidPasswordUiState) { is ValidPasswordUiState.Success -> Text( text = passwordValidationState.result.reason ?: stringResource(R.string.nc_password_secure), - color = if ((passwordValidationState as ValidPasswordUiState.Success).result.passed == false) { + color = if ((passwordValidationState).result.passed == false) { colorResource( id = R.color .nc_darkRed @@ -763,11 +799,15 @@ fun PasswordValidationMessage(passwordValidationState: ValidPasswordUiState) { } else { colorResource(id = R.color.nc_darkGreen) }, + style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth() ) is ValidPasswordUiState.Error -> { - Text(text = passwordValidationState.message) + Text( + text = passwordValidationState.message, + style = MaterialTheme.typography.bodySmall + ) } else -> { diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt index 769293e1959..bfe2b5a5ffc 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt @@ -8,10 +8,30 @@ */ package com.nextcloud.talk.conversationinfo +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context.CLIPBOARD_SERVICE import android.util.Log -import android.view.LayoutInflater import android.view.View 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 +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.lifecycle.LifecycleOwner import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -19,7 +39,6 @@ import com.nextcloud.talk.R import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationInfoBinding -import com.nextcloud.talk.databinding.DialogPasswordBinding import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.conversations.ConversationEnums @@ -41,9 +60,14 @@ class GuestAccessHelper( private val lifecycleOwner: LifecycleOwner ) { private val conversationsRepository = activity.conversationsRepository - private val viewThemeUtils = activity.viewThemeUtils private val context = activity.context + private var shouldCopyPasswordAfterSet: Boolean = false + private var lastSetPassword: String = "" + private var passwordValidationState by mutableStateOf( + ConversationInfoViewModel.SecurePasswordViewState.None + ) + fun setupGuestAccess() { if (ConversationUtils.canModerate(conversation, spreedCapabilities)) { binding.guestAccessView.guestAccessSettings.visibility = View.VISIBLE @@ -62,36 +86,40 @@ class GuestAccessHelper( hideAllOptions() } - binding.guestAccessView.guestAccessSettingsAllowGuest.setOnClickListener { - val isChecked = binding.guestAccessView.allowGuestsSwitch.isChecked - binding.guestAccessView.allowGuestsSwitch.isChecked = !isChecked - viewModel.allowGuests( - conversationUser, - conversation.token, - !isChecked - ) - viewModel.allowGuestsViewState.observe(lifecycleOwner) { uiState -> - when (uiState) { - is ConversationInfoViewModel.AllowGuestsUIState.Success -> { - binding.guestAccessView.allowGuestsSwitch.isChecked = uiState.allow - if (uiState.allow) { - showAllOptions() - } else { - hideAllOptions() - } - } - is ConversationInfoViewModel.AllowGuestsUIState.Error -> { - val exception = uiState.exception - val message = context.getString(R.string.nc_guest_access_allow_failed) - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() - Log.e(TAG, message, exception) - } - ConversationInfoViewModel.AllowGuestsUIState.None -> { + viewModel.allowGuestsViewState.observe(lifecycleOwner) { uiState -> + when (uiState) { + is ConversationInfoViewModel.AllowGuestsUIState.Success -> { + binding.guestAccessView.allowGuestsSwitch.isChecked = uiState.allow + if (uiState.allow) { + showAllOptions() + } else { + hideAllOptions() } } + + is ConversationInfoViewModel.AllowGuestsUIState.Error -> { + val exception = uiState.exception + val message = context.getString(R.string.nc_guest_access_allow_failed) + Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() + Log.e(TAG, message, exception) + } + + ConversationInfoViewModel.AllowGuestsUIState.None -> Unit } } + viewModel.securePasswordViewState.observe(lifecycleOwner) { uiState -> + passwordValidationState = uiState + } + + passwordObserver() + + binding.guestAccessView.guestAccessSettingsAllowGuest.setOnClickListener { + val isChecked = binding.guestAccessView.allowGuestsSwitch.isChecked + binding.guestAccessView.allowGuestsSwitch.isChecked = !isChecked + viewModel.allowGuests(conversationUser, conversation.token, !isChecked) + } + binding.guestAccessView.guestAccessSettingsPasswordProtection.setOnClickListener { val isChecked = binding.guestAccessView.passwordProtectionSwitch.isChecked binding.guestAccessView.passwordProtectionSwitch.isChecked = !isChecked @@ -105,12 +133,7 @@ class GuestAccessHelper( conversationUser.baseUrl!!, conversation.token ) - viewModel.setPassword( - user = conversationUser, - url = url, - password = "" - ) - passwordObserver() + viewModel.setPassword(user = conversationUser, url = url, password = "") } else { showPasswordDialog() } @@ -124,11 +147,10 @@ class GuestAccessHelper( conversation.token ) - conversationsRepository.resendInvitations( - user = conversationUser, - url = url - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()).subscribe(ResendInvitationsObserver()) + conversationsRepository.resendInvitations(user = conversationUser, url = url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ResendInvitationsObserver()) } } @@ -136,61 +158,64 @@ class GuestAccessHelper( viewModel.passwordViewState.observe(lifecycleOwner) { uiState -> when (uiState) { is ConversationInfoViewModel.PasswordUiState.Success -> { - // unused atm + if (shouldCopyPasswordAfterSet && lastSetPassword.isNotEmpty()) { + val clipboardManager = activity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("Guest access password", lastSetPassword) + clipboardManager.setPrimaryClip(clipData) + } + shouldCopyPasswordAfterSet = false + lastSetPassword = "" } + is ConversationInfoViewModel.PasswordUiState.Error -> { val exception = uiState.exception val message = context.getString(R.string.nc_guest_access_password_failed) Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() Log.e(TAG, message, exception) } - is ConversationInfoViewModel.PasswordUiState.None -> { - // unused atm - } + + is ConversationInfoViewModel.PasswordUiState.None -> Unit } } } private fun showPasswordDialog() { - val builder = MaterialAlertDialogBuilder(activity) - builder.apply { - val dialogPassword = DialogPasswordBinding.inflate(LayoutInflater.from(context)) - viewThemeUtils.platform.colorEditText(dialogPassword.password) - setView(dialogPassword.root) - setTitle(R.string.nc_guest_access_password_dialog_title) - setPositiveButton(R.string.nc_ok) { _, _ -> - val apiVersion = ApiUtils.getConversationApiVersion( - conversationUser, - intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1) - ) - val url = ApiUtils.getUrlForRoomPassword( - apiVersion, - conversationUser.baseUrl!!, - conversation.token - ) - val password = dialogPassword.password.text.toString() - viewModel.setPassword( - user = conversationUser, - url = url, - password = password - ) - } - setNegativeButton(R.string.nc_cancel) { _, _ -> - binding.guestAccessView.passwordProtectionSwitch.isChecked = false - } + val apiVersion = ApiUtils.getConversationApiVersion( + conversationUser, + intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1) + ) + val url = ApiUtils.getUrlForRoomPassword(apiVersion, conversationUser.baseUrl!!, conversation.token) + + val validPasswordUrl = conversationUser?.capabilities?.passwordCapability?.api?.validatePasswordApi ?: "" + passwordValidationState = ConversationInfoViewModel.SecurePasswordViewState.None + + val composeView = ComposeView(activity) + var materialDialog: AlertDialog? = null + val credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token) + composeView.setContent { + GuestAccessPasswordDialog( + validationState = passwordValidationState, + onPasswordChanged = { password -> + viewModel.securePassword(credentials!!, validPasswordUrl, password) + }, + onDismiss = { + binding.guestAccessView.passwordProtectionSwitch.isChecked = false + materialDialog?.dismiss() + }, + onSave = { password, copyAfterSave -> + shouldCopyPasswordAfterSet = copyAfterSave + lastSetPassword = password + viewModel.setPassword(user = conversationUser, url = url, password = password) + materialDialog?.dismiss() + } + ) } - createDialog(builder) - passwordObserver() - } - private fun createDialog(builder: MaterialAlertDialogBuilder) { - builder.create() - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.conversationInfoName.context, builder) - val dialog = builder.show() - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - ) + val builder = MaterialAlertDialogBuilder(activity) + .setView(composeView) + .setCancelable(true) + + materialDialog = builder.show() } inner class ResendInvitationsObserver : Observer { @@ -236,3 +261,89 @@ class GuestAccessHelper( private val TAG = GuestAccessHelper::class.simpleName } } + +@Composable +private fun GuestAccessPasswordDialog( + validationState: ConversationInfoViewModel.SecurePasswordViewState, + onPasswordChanged: (String) -> Unit, + onDismiss: () -> Unit, + onSave: (password: String, copyAfterSave: Boolean) -> Unit +) { + var password by rememberSaveable { mutableStateOf("") } + + val warningMessage = when (validationState) { + is ConversationInfoViewModel.SecurePasswordViewState.Success -> { + validationState.result.passed?.let { validPassword -> + if (!validPassword) { + validationState.result.reason + } else { + stringResource(R.string.nc_password_secure) + } + } + } + + is ConversationInfoViewModel.SecurePasswordViewState.Error -> { + stringResource(id = R.string.nc_common_error_sorry) + } + + ConversationInfoViewModel.SecurePasswordViewState.None -> "" + } + + val isPasswordValid = + password.isNotBlank() && warningMessage == stringResource(R.string.nc_password_secure) + + AlertDialog( + 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)) + } + } + ) +} 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 638ac3b557f..46df92d7e25 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 @@ -15,6 +15,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationcreation.ConversationCreationRepository import com.nextcloud.talk.conversationinfo.CreateRoomRequest import com.nextcloud.talk.conversationinfo.Participants import com.nextcloud.talk.data.user.model.User @@ -28,6 +29,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS 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.utils.ApiUtils @@ -43,7 +45,8 @@ import javax.inject.Inject @Suppress("TooManyFunctions") class ConversationInfoViewModel @Inject constructor( private val chatNetworkDataSource: ChatNetworkDataSource, - private val conversationsRepository: ConversationsRepository + private val conversationsRepository: ConversationsRepository, + private val conversationCreationRepository: ConversationCreationRepository ) : ViewModel() { object LifeCycleObserver : DefaultLifecycleObserver { @@ -111,6 +114,9 @@ class ConversationInfoViewModel @Inject constructor( val passwordViewState: LiveData get() = _passwordViewState + private val _securePasswordViewState = MutableLiveData(SecurePasswordViewState.None) + val securePasswordViewState: LiveData = _securePasswordViewState + private val _getCapabilitiesViewState: MutableLiveData = MutableLiveData(GetCapabilitiesStartState) val getCapabilitiesViewState: LiveData get() = _getCapabilitiesViewState @@ -167,6 +173,10 @@ class ConversationInfoViewModel @Inject constructor( ?.subscribe(GetRoomObserver()) } + fun resetSecurePasswordViewState() { + _securePasswordViewState.value = SecurePasswordViewState.None + } + @Suppress("Detekt.TooGenericExceptionCaught") fun createRoomFromOneToOne( user: User, @@ -213,6 +223,23 @@ 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() @@ -568,4 +595,10 @@ class ConversationInfoViewModel @Inject constructor( 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/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml index a4647154ae5..70efa9e5199 100644 --- a/app/src/main/res/layout/dialog_password.xml +++ b/app/src/main/res/layout/dialog_password.xml @@ -24,4 +24,16 @@ android:hint="@string/nc_guest_access_password_dialog_hint" android:inputType="textPassword" android:importantForAutofill="no" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3dc61ad5b4b..5ef79f41dee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -960,4 +960,5 @@ How to translate with transifex: Show app switcher Nextcloud app suggestions in account chooser dialog Password is secure + Copy password From 46cd60a8111bf9efc06213b2674f0bd46d421d70 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Thu, 12 Mar 2026 11:04:07 +0100 Subject: [PATCH 5/5] remove unused function and add copyright info Signed-off-by: sowjanyakch --- .../json/capabilities/PasswordAccount.kt | 2 +- .../models/json/capabilities/PasswordApi.kt | 2 +- .../json/capabilities/PasswordCapability.kt | 2 +- .../json/capabilities/PasswordPolicies.kt | 2 +- .../json/capabilities/PasswordPolicy.kt | 2 +- .../json/passwordResult/PasswordResult.kt | 2 +- .../json/passwordResult/PasswordResultOCS.kt | 2 +- .../passwordResult/PasswordResultOverall.kt | 2 +- app/src/main/res/layout/dialog_password.xml | 39 ------------------- 9 files changed, 8 insertions(+), 47 deletions(-) delete mode 100644 app/src/main/res/layout/dialog_password.xml 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 index df468ec9ed0..27d01245db3 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index 01a4118b945..9ba832ac210 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index 9c7732c94b8..aabc10a719a 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index f656554b278..98b47cc4954 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index 477d0b5aff3..8bbc86fd144 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index d73222645f8..cc45672141f 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index 8742cca1035..d967656a243 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 index 45f1904ead6..5f202864c99 100644 --- 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 @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later */ 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 70efa9e5199..00000000000 --- a/app/src/main/res/layout/dialog_password.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - -