Skip to content

Commit

Permalink
feat: show pending legal hold request and approve it [WPB-4393] (#2484)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk committed Nov 30, 2023
1 parent 2dd5d83 commit b51377f
Show file tree
Hide file tree
Showing 9 changed files with 501 additions and 37 deletions.
Expand Up @@ -435,8 +435,7 @@ class WireActivityViewModel @Inject constructor(
CurrentScreen.InBackground,
is CurrentScreen.Conversation,
CurrentScreen.Home,
is CurrentScreen.IncomingCallScreen,
is CurrentScreen.OngoingCallScreen,
is CurrentScreen.CallScreen,
is CurrentScreen.OtherUserProfile,
CurrentScreen.AuthRelated,
CurrentScreen.SomeOther -> true
Expand Down
Expand Up @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.conversation.LegalHoldStatus
import com.wire.kalium.logic.data.sync.SyncState
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -77,6 +78,16 @@ class CommonTopAppBarViewModel @Inject constructor(
}
}

private fun legalHoldStatusFlow(userId: UserId) = coreLogic.sessionScope(userId) {
observeLegalHoldRequest() // TODO combine with legal hold status
.map { legalHoldRequestResult ->
when (legalHoldRequestResult) {
is ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable -> LegalHoldStatus.PENDING
else -> LegalHoldStatus.DISABLED
}
}
}

init {
viewModelScope.launch {
coreLogic.globalScope {
Expand All @@ -90,10 +101,11 @@ class CommonTopAppBarViewModel @Inject constructor(
combine(
activeCallFlow(userId),
currentScreenFlow(),
connectivityFlow(userId)
) { activeCall, currentScreen, connectivity ->
connectivityFlow(userId),
legalHoldStatusFlow(userId),
) { activeCall, currentScreen, connectivity, legalHoldStatus ->
mapToConnectivityUIState(currentScreen, connectivity, activeCall) to
mapToLegalHoldUIState(currentScreen, LegalHoldStatus.NO_CONSENT) // TODO: get legalHoldStatus
mapToLegalHoldUIState(currentScreen, legalHoldStatus)
}
}
}
Expand Down Expand Up @@ -148,7 +160,7 @@ class CommonTopAppBarViewModel @Inject constructor(
LegalHoldStatus.DISABLED,
LegalHoldStatus.NO_CONSENT -> LegalHoldUIState.None
}.let { legalHoldUIState ->
if (currentScreen is CurrentScreen.AuthRelated) LegalHoldUIState.None
if (currentScreen is CurrentScreen.AuthRelated || currentScreen is CurrentScreen.CallScreen) LegalHoldUIState.None
else legalHoldUIState
}

Expand Down
Expand Up @@ -51,6 +51,7 @@ import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.extension.formatAsFingerPrint
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.data.user.UserId

@OptIn(ExperimentalComposeUiApi::class)
@Composable
Expand Down Expand Up @@ -141,7 +142,11 @@ fun LegalHoldRequestedDialog(
fun PreviewLegalHoldRequestedDialogWithPassword() {
WireTheme {
LegalHoldRequestedDialog(
LegalHoldRequestedState.Visible(legalHoldDeviceFingerprint = "0123456789ABCDEF", requiresPassword = true), {}, {}, {}
LegalHoldRequestedState.Visible(
legalHoldDeviceFingerprint = "0123456789ABCDEF",
requiresPassword = true,
userId = UserId("", ""),
), {}, {}, {}
)
}
}
Expand All @@ -151,7 +156,11 @@ fun PreviewLegalHoldRequestedDialogWithPassword() {
fun PreviewLegalHoldRequestedDialogWithoutPassword() {
WireTheme {
LegalHoldRequestedDialog(
LegalHoldRequestedState.Visible(legalHoldDeviceFingerprint = "0123456789ABCDEF", requiresPassword = false), {}, {}, {}
LegalHoldRequestedState.Visible(
legalHoldDeviceFingerprint = "0123456789ABCDEF",
requiresPassword = false,
userId = UserId("", ""),
), {}, {}, {}
)
}
}
Expand Up @@ -19,6 +19,7 @@ package com.wire.android.ui.legalhold.dialog.requested

import androidx.compose.ui.text.input.TextFieldValue
import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.user.UserId

sealed class LegalHoldRequestedState {
data object Hidden : LegalHoldRequestedState()
Expand All @@ -29,6 +30,7 @@ sealed class LegalHoldRequestedState {
val loading: Boolean = false,
val acceptEnabled: Boolean = false,
val error: LegalHoldRequestedError = LegalHoldRequestedError.None,
val userId: UserId,
) : LegalHoldRequestedState()
}

Expand Down
Expand Up @@ -26,10 +26,21 @@ import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.UserSessionScope
import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldRequestUseCase
import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCaseResult
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -41,7 +52,62 @@ class LegalHoldRequestedViewModel @Inject constructor(

var state: LegalHoldRequestedState by mutableStateOf(LegalHoldRequestedState.Hidden)
private set
// TODO: get legal hold status of current account

private val legalHoldRequestDataStateFlow = currentSessionFlow(noSession = LegalHoldRequestData.None) { userId ->
observeLegalHoldRequest()
.mapLatest { legalHoldRequestResult ->
when (legalHoldRequestResult) {
is ObserveLegalHoldRequestUseCaseResult.Failure -> {
appLogger.e("$TAG: Failed to get legal hold request data: ${legalHoldRequestResult.failure}")
LegalHoldRequestData.None
}

ObserveLegalHoldRequestUseCaseResult.NoObserveLegalHoldRequest -> LegalHoldRequestData.None
is ObserveLegalHoldRequestUseCaseResult.ObserveLegalHoldRequestAvailable ->
users.isPasswordRequired()
.let {
LegalHoldRequestData.Pending(
legalHoldRequestResult.fingerprint.decodeToString(),
(it as? IsPasswordRequiredUseCase.Result.Success)?.value ?: true,
userId,
)
}
}
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, LegalHoldRequestData.None)

private fun <T> currentSessionFlow(noSession: T, session: UserSessionScope.(UserId) -> Flow<T>): Flow<T> =
coreLogic.getGlobalScope().session.currentSessionFlow()
.flatMapLatest { currentSessionResult ->
when (currentSessionResult) {
is CurrentSessionResult.Failure.Generic -> {
appLogger.e("$TAG: Failed to get current session")
flowOf(noSession)
}

CurrentSessionResult.Failure.SessionNotFound -> flowOf(noSession)
is CurrentSessionResult.Success ->
currentSessionResult.accountInfo.userId.let { coreLogic.getSessionScope(it).session(it) }
}
}

init {
viewModelScope.launch {
legalHoldRequestDataStateFlow.collectLatest { legalHoldRequestData ->
state = when (legalHoldRequestData) {
is LegalHoldRequestData.Pending -> {
LegalHoldRequestedState.Visible(
requiresPassword = legalHoldRequestData.isPasswordRequired,
legalHoldDeviceFingerprint = legalHoldRequestData.fingerprint,
userId = legalHoldRequestData.userId,
)
}

LegalHoldRequestData.None -> LegalHoldRequestedState.Hidden
}
}
}
}

private fun LegalHoldRequestedState.ifVisible(action: (LegalHoldRequestedState.Visible) -> Unit) {
if (this is LegalHoldRequestedState.Visible) action(this)
Expand All @@ -57,25 +123,13 @@ class LegalHoldRequestedViewModel @Inject constructor(
state = LegalHoldRequestedState.Hidden
}

private suspend fun checkIfPasswordRequired(action: (Boolean) -> Unit) {
when (val currentSessionResult = coreLogic.getGlobalScope().session.currentSession()) {
CurrentSessionResult.Failure.SessionNotFound -> appLogger.e("$TAG: Session not found")
is CurrentSessionResult.Failure.Generic -> appLogger.e("$TAG: Failed to get current session")
is CurrentSessionResult.Success -> action(
coreLogic.getSessionScope(currentSessionResult.accountInfo.userId).users.isPasswordRequired()
.let { (it as? IsPasswordRequiredUseCase.Result.Success)?.value ?: true }
)
}
}

fun show() {
viewModelScope.launch {
checkIfPasswordRequired { isPasswordRequired ->
state = LegalHoldRequestedState.Visible(
requiresPassword = isPasswordRequired,
legalHoldDeviceFingerprint = "0123456789ABCDEF" // TODO: get legal hold client fingerprint
)
}
(legalHoldRequestDataStateFlow.value as? LegalHoldRequestData.Pending)?.let {
state = LegalHoldRequestedState.Visible(
requiresPassword = it.isPasswordRequired,
legalHoldDeviceFingerprint = it.fingerprint,
userId = it.userId,
)
}
}

Expand All @@ -84,14 +138,55 @@ class LegalHoldRequestedViewModel @Inject constructor(
state = it.copy(acceptEnabled = false, loading = true)
// the accept button is enabled if the password is valid, this check is for safety only
validatePassword(it.password.text).let { validatePasswordResult ->
state = when (validatePasswordResult.isValid) {
false -> it.copy(loading = false, error = LegalHoldRequestedError.InvalidCredentialsError)
true -> LegalHoldRequestedState.Hidden // TODO: accept legal hold
when (validatePasswordResult.isValid) {
false ->
state = it.copy(
loading = false,
error = LegalHoldRequestedError.InvalidCredentialsError
)

true ->
viewModelScope.launch {
coreLogic.sessionScope(it.userId) {
approveLegalHoldRequest(it.password.text).let { approveLegalHoldResult ->
state = when (approveLegalHoldResult) {
is ApproveLegalHoldRequestUseCase.Result.Success ->
LegalHoldRequestedState.Hidden

ApproveLegalHoldRequestUseCase.Result.Failure.InvalidPassword ->
it.copy(
loading = false,
error = LegalHoldRequestedError.InvalidCredentialsError
)

ApproveLegalHoldRequestUseCase.Result.Failure.PasswordRequired ->
it.copy(
loading = false,
requiresPassword = true,
error = LegalHoldRequestedError.InvalidCredentialsError
)

is ApproveLegalHoldRequestUseCase.Result.Failure.GenericFailure -> {
appLogger.e("$TAG: Failed to approve legal hold: ${approveLegalHoldResult.coreFailure}")
it.copy(
loading = false,
error = LegalHoldRequestedError.GenericError(approveLegalHoldResult.coreFailure)
)
}
}
}
}
}
}
}
}
}

private sealed class LegalHoldRequestData {
data object None : LegalHoldRequestData()
data class Pending(val fingerprint: String, val isPasswordRequired: Boolean, val userId: UserId) : LegalHoldRequestData()
}

companion object {
private const val TAG = "LegalHoldRequestedViewModel"
}
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
Expand Up @@ -40,6 +40,7 @@ import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
import com.wire.android.ui.destinations.IncomingCallScreenDestination
import com.wire.android.ui.destinations.InitialSyncScreenDestination
import com.wire.android.ui.destinations.InitiatingCallScreenDestination
import com.wire.android.ui.destinations.LoginScreenDestination
import com.wire.android.ui.destinations.MigrationScreenDestination
import com.wire.android.ui.destinations.OngoingCallScreenDestination
Expand Down Expand Up @@ -151,11 +152,16 @@ sealed class CurrentScreen {
// Another User Profile Screen is opened
data class OtherUserProfile(val id: ConversationId) : CurrentScreen()

sealed class CallScreen(open val id: QualifiedID) : CurrentScreen()

// Ongoing call screen is opened
data class OngoingCallScreen(val id: QualifiedID) : CurrentScreen()
class OngoingCallScreen(override val id: QualifiedID) : CallScreen(id)

// Incoming call screen is opened
data class IncomingCallScreen(val id: QualifiedID) : CurrentScreen()
class IncomingCallScreen(override val id: QualifiedID) : CallScreen(id)

// Initiating call screen is opened
class InitiatingCallScreen(override val id: QualifiedID) : CallScreen(id)

// Import media screen is opened
object ImportMedia : CurrentScreen()
Expand Down Expand Up @@ -193,6 +199,9 @@ sealed class CurrentScreen {
is IncomingCallScreenDestination ->
IncomingCallScreen(destination.argsFrom(arguments).conversationId)

is InitiatingCallScreenDestination ->
InitiatingCallScreen(destination.argsFrom(arguments).conversationId)

is ImportMediaScreenDestination -> ImportMedia

is SelfDevicesScreenDestination -> DeviceManager
Expand Down

0 comments on commit b51377f

Please sign in to comment.