Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -577,4 +578,12 @@ interface NcApiCoroutines {
@FormUrlEncoded
@POST
suspend fun reportRemoteWipeSuccess(@Url url: String, @Field("token") token: String): Response<Unit>

@FormUrlEncoded
@POST
suspend fun validatePassword(
@Header("Authorization") authorization: String,
@Url url: String,
@Field("password") password: String
): PasswordResultOverall
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +35,9 @@ class ConversationCreationViewModel @Inject constructor(
val selectedParticipants: StateFlow<List<AutocompleteUser>> = _selectedParticipants
private val roomViewState = MutableStateFlow<RoomUIState>(RoomUIState.None)

private val _validPasswordViewState = MutableStateFlow<ValidPasswordUiState>(ValidPasswordUiState.None)
val validPasswordViewState: StateFlow<ValidPasswordUiState> = _validPasswordViewState

private val _selectedImageUri = MutableStateFlow<Uri?>(null)
val selectedImageUri: StateFlow<Uri?> = _selectedImageUri

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -285,3 +311,9 @@ sealed class AddParticipantsUiState {
data class Success(val participants: List<Conversation>?) : 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -194,6 +214,8 @@ class ConversationInfoActivity : BaseActivity() {
}
}

viewModel.securePasswordViewState.observe(this) { securePasswordViewState = it }

setupCompose()
}

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -287,7 +352,7 @@ class ConversationInfoActivity : BaseActivity() {
password = ""
)
} else {
showPasswordDialog(conversationToken)
onShowPasswordDialog()
}
},
onResendInvitationsClick = {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 -> ""
}
Loading
Loading