Skip to content
Open
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -52,9 +53,9 @@ import com.wire.android.navigation.WireNavigator
import com.wire.android.navigation.annotation.features.cells.WireDestination
import com.wire.android.navigation.style.PopUpNavigationAnimation
import com.wire.android.ui.common.HandleActions
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.button.WireButtonState.Default
import com.wire.android.ui.common.button.WireButtonState.Disabled
import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.chip.ChipData
import com.wire.android.ui.common.chip.WireFilterChip
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions
Expand All @@ -80,6 +81,8 @@ fun AddRemoveTagsScreen(
) {
val context = LocalContext.current

val viewState by addRemoveTagsViewModel.state.collectAsState()

WireScaffold(
modifier = modifier,
snackbarHost = {},
Expand All @@ -90,21 +93,20 @@ fun AddRemoveTagsScreen(
onNavigationPressed = {
navigator.navigateBack()
},
elevation = dimensions().spacing0x,
)
},
bottomBar = {
val isLoading = addRemoveTagsViewModel.isLoading.collectAsState().value
val tags = addRemoveTagsViewModel.suggestedTags
Column(
modifier = Modifier.background(colorsScheme().background)
) {
if (tags.isNotEmpty()) {
if (viewState.suggestedTags.isNotEmpty()) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(start = dimensions().spacing16x, end = dimensions().spacing16x)
) {
tags.forEach { tag ->
viewState.suggestedTags.forEach { tag ->
item {
WireFilterChip(
modifier = Modifier.padding(
Expand All @@ -131,15 +133,13 @@ fun AddRemoveTagsScreen(
.padding(horizontal = dimensions().spacing16x)
.height(dimensions().groupButtonHeight)
) {
val shouldDisabledSaveButton =
isLoading || addRemoveTagsViewModel.initialTags == addRemoveTagsViewModel.addedTags.collectAsState().value
WirePrimaryButton(
text = stringResource(R.string.save_label),
onClick = {
addRemoveTagsViewModel.updateTags()
},
state = if (shouldDisabledSaveButton) WireButtonState.Disabled else WireButtonState.Default,
loading = isLoading,
state = if (viewState.tagsUpdated && !viewState.isLoading) Default else Disabled,
loading = viewState.isLoading,
clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true),
)
}
Expand All @@ -151,7 +151,7 @@ fun AddRemoveTagsScreen(
AddRemoveTagsScreenContent(
internalPadding = internalPadding,
textFieldState = addRemoveTagsViewModel.tagsTextState,
addedTags = addRemoveTagsViewModel.addedTags.collectAsState().value,
addedTags = viewState.addedTags,
onAddTag = { tag ->
addRemoveTagsViewModel.addTag(tag)
},
Expand Down Expand Up @@ -217,18 +217,20 @@ fun AddRemoveTagsScreenContent(

ChipAndTextFieldLayout(
textFieldState = textFieldState,
tags = addedTags.map { ChipData(label = it, isSelected = true, isEnabled = false) }.toSet(),
isValidTag = isValidTag,
onDone = onAddTag,
onRemoveLastTag = onRemoveLastTag,
) { data: ChipData ->

WireFilterChip(
label = data.label,
isSelected = data.isSelected,
onSelectChip = onRemoveTag
)
}
chipsLayout = {
addedTags.forEach { item ->
WireFilterChip(
modifier = Modifier.align(Alignment.CenterVertically),
label = item,
isSelected = true,
onSelectChip = onRemoveTag
)
}
}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ package com.wire.android.feature.cells.ui.tags

import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
Expand All @@ -30,11 +27,11 @@ import com.wire.android.ui.common.ActionsViewModel
import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase
import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase
import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase
import com.wire.kalium.common.functional.getOrElse
import com.wire.kalium.common.functional.onFailure
import com.wire.kalium.common.functional.onSuccess
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update
Expand All @@ -50,98 +47,93 @@ class AddRemoveTagsViewModel @Inject constructor(
) : ActionsViewModel<AddRemoveTagsViewModelAction>() {

private val navArgs: AddRemoveTagsNavArgs = savedStateHandle.navArgs()
private val initialTags: Set<String> = navArgs.tags.toSet()
private val disallowedChars = listOf(",", ";", "/", "\\", "\"", "\'", "<", ">")

val isLoading = MutableStateFlow(false)
private val _state = MutableStateFlow(TagsViewState(addedTags = initialTags))
val state = _state.asStateFlow()

val tagsTextState = TextFieldState()

val initialTags: Set<String> = navArgs.tags.toSet()

val disallowedChars = listOf(",", ";", "/", "\\", "\"", "\'", "<", ">")

var allTags: Set<String> = emptySet()
private set

val addedTags: MutableStateFlow<Set<String>> = MutableStateFlow(navArgs.tags.toSet())

var suggestedTags by mutableStateOf<Set<String>>(emptySet())
private set

init {
viewModelScope.launch {
allTags = getAllTagsUseCase().getOrElse { emptySet() }
updateSuggestions("") // initial state
}

viewModelScope.launch {
snapshotFlow { tagsTextState.text.toString() }
.debounce(TYPING_DEBOUNCE_TIME)
.collectLatest {
onQueryChanged(it)
}
getAllTagsUseCase().onSuccess { tags ->
_state.update { it.copy(allTags = tags) }
}
launch {
snapshotFlow { tagsTextState.text.toString() }
.debounce(TYPING_DEBOUNCE_TIME)
.collectLatest { updateViewState() }
}
}
}

fun onQueryChanged(query: String) {
updateSuggestions(query)
fun isValidTag(): Boolean = with(tagsTextState) {
disallowedChars.none { it in text } && text.length in ALLOWED_LENGTH
}

private fun updateSuggestions(query: String) {
suggestedTags = allTags
.asSequence()
.filter { query.isBlank() || it.contains(query, ignoreCase = true) }
.filter { it !in addedTags.value }
.toSet()
}

fun isValidTag(): Boolean = disallowedChars.none {
it in tagsTextState.text
} && tagsTextState.text.length in ALLOWED_LENGTH

fun addTag(tag: String) {
tag.trim().let { newTag ->
if (newTag.isNotBlank() && newTag !in addedTags.value) {
addedTags.update { it + newTag }
updateSuggestions("")
val addedTags = state.value.addedTags
if (newTag.isNotBlank() && newTag !in addedTags) {
updateViewState(addedTags + tag)
tagsTextState.clearText()
}
}
}

fun removeTag(tag: String) {
addedTags.update { it - tag }
updateSuggestions("")
updateViewState(state.value.addedTags - tag)
}

fun removeLastTag() {
addedTags.value.lastOrNull()?.let { lastTag ->
removeTag(lastTag)
state.value.addedTags.lastOrNull()?.let { removeTag(it) }
}

fun updateTags() = viewModelScope.launch {
_state.update { it.copy(isLoading = true) }

if (state.value.addedTags.isEmpty()) {
removeNodeTagsUseCase(navArgs.uuid)
} else {
updateNodeTagsUseCase(navArgs.uuid, state.value.addedTags)
}
.onSuccess { sendAction(AddRemoveTagsViewModelAction.Success) }
.onFailure { sendAction(AddRemoveTagsViewModelAction.Failure) }
.also { _state.update { it.copy(isLoading = false) } }
}

fun updateTags() {
viewModelScope.launch {
isLoading.value = true
val result = if (addedTags.value.isEmpty()) {
removeNodeTagsUseCase(navArgs.uuid)
} else {
updateNodeTagsUseCase(navArgs.uuid, addedTags.value)
}
fun updateViewState() {
updateViewState(state.value.addedTags)
}

result
.onSuccess { sendAction(AddRemoveTagsViewModelAction.Success) }
.onFailure { sendAction(AddRemoveTagsViewModelAction.Failure) }
.also { isLoading.value = false }
fun updateViewState(addedTags: Set<String>) {
_state.update { current ->
current.copy(
addedTags = addedTags,
suggestedTags = current.allTags
.filter { it !in addedTags }
.filter { it.contains(tagsTextState.text.toString(), ignoreCase = true) }
.toSet(),
tagsUpdated = addedTags != initialTags
)
}
}

companion object {
private companion object {
val ALLOWED_LENGTH = 1..30

const val TYPING_DEBOUNCE_TIME = 200L
}
}

data class TagsViewState(
val isLoading: Boolean = false,
val tagsUpdated: Boolean = false,
val addedTags: Set<String> = emptySet(),
val suggestedTags: Set<String> = emptySet(),
val allTags: Set<String> = emptySet(),
)

sealed interface AddRemoveTagsViewModelAction {
data object Success : AddRemoveTagsViewModelAction
data object Failure : AddRemoveTagsViewModelAction
Expand Down
Loading