Skip to content

Commit 3f3f5ac

Browse files
authored
chore: tags ui refactor (WPB-22984) (#4467)
1 parent 91c8a4f commit 3f3f5ac

5 files changed

Lines changed: 144 additions & 164 deletions

File tree

core/ui-common/src/main/kotlin/com/wire/android/ui/common/chip/ChipData.kt

Lines changed: 0 additions & 29 deletions
This file was deleted.

features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import androidx.compose.material3.Text
3838
import androidx.compose.runtime.Composable
3939
import androidx.compose.runtime.LaunchedEffect
4040
import androidx.compose.runtime.collectAsState
41+
import androidx.compose.runtime.getValue
42+
import androidx.compose.runtime.key
4143
import androidx.compose.runtime.rememberCoroutineScope
4244
import androidx.compose.ui.Alignment
4345
import androidx.compose.ui.Modifier
@@ -52,9 +54,9 @@ import com.wire.android.navigation.WireNavigator
5254
import com.wire.android.navigation.annotation.features.cells.WireDestination
5355
import com.wire.android.navigation.style.PopUpNavigationAnimation
5456
import com.wire.android.ui.common.HandleActions
55-
import com.wire.android.ui.common.button.WireButtonState
57+
import com.wire.android.ui.common.button.WireButtonState.Default
58+
import com.wire.android.ui.common.button.WireButtonState.Disabled
5659
import com.wire.android.ui.common.button.WirePrimaryButton
57-
import com.wire.android.ui.common.chip.ChipData
5860
import com.wire.android.ui.common.chip.WireFilterChip
5961
import com.wire.android.ui.common.colorsScheme
6062
import com.wire.android.ui.common.dimensions
@@ -80,6 +82,8 @@ fun AddRemoveTagsScreen(
8082
) {
8183
val context = LocalContext.current
8284

85+
val viewState by addRemoveTagsViewModel.state.collectAsState()
86+
8387
WireScaffold(
8488
modifier = modifier,
8589
snackbarHost = {},
@@ -90,21 +94,20 @@ fun AddRemoveTagsScreen(
9094
onNavigationPressed = {
9195
navigator.navigateBack()
9296
},
97+
elevation = dimensions().spacing0x,
9398
)
9499
},
95100
bottomBar = {
96-
val isLoading = addRemoveTagsViewModel.isLoading.collectAsState().value
97-
val tags = addRemoveTagsViewModel.suggestedTags
98101
Column(
99102
modifier = Modifier.background(colorsScheme().background)
100103
) {
101-
if (tags.isNotEmpty()) {
104+
if (viewState.suggestedTags.isNotEmpty()) {
102105
LazyRow(
103106
modifier = Modifier
104107
.fillMaxWidth()
105108
.padding(start = dimensions().spacing16x, end = dimensions().spacing16x)
106109
) {
107-
tags.forEach { tag ->
110+
viewState.suggestedTags.forEach { tag ->
108111
item {
109112
WireFilterChip(
110113
modifier = Modifier.padding(
@@ -131,15 +134,13 @@ fun AddRemoveTagsScreen(
131134
.padding(horizontal = dimensions().spacing16x)
132135
.height(dimensions().groupButtonHeight)
133136
) {
134-
val shouldDisabledSaveButton =
135-
isLoading || addRemoveTagsViewModel.initialTags == addRemoveTagsViewModel.addedTags.collectAsState().value
136137
WirePrimaryButton(
137138
text = stringResource(R.string.save_label),
138139
onClick = {
139140
addRemoveTagsViewModel.updateTags()
140141
},
141-
state = if (shouldDisabledSaveButton) WireButtonState.Disabled else WireButtonState.Default,
142-
loading = isLoading,
142+
state = if (viewState.tagsUpdated && !viewState.isLoading) Default else Disabled,
143+
loading = viewState.isLoading,
143144
clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true),
144145
)
145146
}
@@ -151,7 +152,7 @@ fun AddRemoveTagsScreen(
151152
AddRemoveTagsScreenContent(
152153
internalPadding = internalPadding,
153154
textFieldState = addRemoveTagsViewModel.tagsTextState,
154-
addedTags = addRemoveTagsViewModel.addedTags.collectAsState().value,
155+
addedTags = viewState.addedTags,
155156
onAddTag = { tag ->
156157
addRemoveTagsViewModel.addTag(tag)
157158
},
@@ -217,18 +218,22 @@ fun AddRemoveTagsScreenContent(
217218

218219
ChipAndTextFieldLayout(
219220
textFieldState = textFieldState,
220-
tags = addedTags.map { ChipData(label = it, isSelected = true, isEnabled = false) }.toSet(),
221221
isValidTag = isValidTag,
222222
onDone = onAddTag,
223223
onRemoveLastTag = onRemoveLastTag,
224-
) { data: ChipData ->
225-
226-
WireFilterChip(
227-
label = data.label,
228-
isSelected = data.isSelected,
229-
onSelectChip = onRemoveTag
230-
)
231-
}
224+
chipsLayout = {
225+
addedTags.forEach { item ->
226+
key(item) {
227+
WireFilterChip(
228+
modifier = Modifier.align(Alignment.CenterVertically),
229+
label = item,
230+
isSelected = true,
231+
onSelectChip = onRemoveTag
232+
)
233+
}
234+
}
235+
}
236+
)
232237
}
233238
}
234239

features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModel.kt

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ package com.wire.android.feature.cells.ui.tags
1919

2020
import androidx.compose.foundation.text.input.TextFieldState
2121
import androidx.compose.foundation.text.input.clearText
22-
import androidx.compose.runtime.getValue
23-
import androidx.compose.runtime.mutableStateOf
24-
import androidx.compose.runtime.setValue
2522
import androidx.compose.runtime.snapshotFlow
2623
import androidx.lifecycle.SavedStateHandle
2724
import androidx.lifecycle.viewModelScope
@@ -30,11 +27,11 @@ import com.wire.android.ui.common.ActionsViewModel
3027
import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase
3128
import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase
3229
import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase
33-
import com.wire.kalium.common.functional.getOrElse
3430
import com.wire.kalium.common.functional.onFailure
3531
import com.wire.kalium.common.functional.onSuccess
3632
import dagger.hilt.android.lifecycle.HiltViewModel
3733
import kotlinx.coroutines.flow.MutableStateFlow
34+
import kotlinx.coroutines.flow.asStateFlow
3835
import kotlinx.coroutines.flow.collectLatest
3936
import kotlinx.coroutines.flow.debounce
4037
import kotlinx.coroutines.flow.update
@@ -50,98 +47,86 @@ class AddRemoveTagsViewModel @Inject constructor(
5047
) : ActionsViewModel<AddRemoveTagsViewModelAction>() {
5148

5249
private val navArgs: AddRemoveTagsNavArgs = savedStateHandle.navArgs()
50+
private val initialTags: Set<String> = navArgs.tags.toSet()
51+
private val disallowedChars = setOf(",", ";", "/", "\\", "\"", "\'", "<", ">")
5352

54-
val isLoading = MutableStateFlow(false)
53+
private val _state = MutableStateFlow(TagsViewState(addedTags = initialTags))
54+
val state = _state.asStateFlow()
5555

5656
val tagsTextState = TextFieldState()
5757

58-
val initialTags: Set<String> = navArgs.tags.toSet()
59-
60-
val disallowedChars = listOf(",", ";", "/", "\\", "\"", "\'", "<", ">")
61-
62-
var allTags: Set<String> = emptySet()
63-
private set
64-
65-
val addedTags: MutableStateFlow<Set<String>> = MutableStateFlow(navArgs.tags.toSet())
66-
67-
var suggestedTags by mutableStateOf<Set<String>>(emptySet())
68-
private set
69-
7058
init {
7159
viewModelScope.launch {
72-
allTags = getAllTagsUseCase().getOrElse { emptySet() }
73-
updateSuggestions("") // initial state
74-
}
75-
76-
viewModelScope.launch {
60+
getAllTagsUseCase().onSuccess { tags ->
61+
_state.update { it.copy(allTags = tags) }
62+
}
7763
snapshotFlow { tagsTextState.text.toString() }
7864
.debounce(TYPING_DEBOUNCE_TIME)
79-
.collectLatest {
80-
onQueryChanged(it)
81-
}
65+
.collectLatest { updateViewState() }
8266
}
8367
}
8468

85-
fun onQueryChanged(query: String) {
86-
updateSuggestions(query)
87-
}
88-
89-
private fun updateSuggestions(query: String) {
90-
suggestedTags = allTags
91-
.asSequence()
92-
.filter { query.isBlank() || it.contains(query, ignoreCase = true) }
93-
.filter { it !in addedTags.value }
94-
.toSet()
69+
fun isValidTag(): Boolean = with(tagsTextState) {
70+
disallowedChars.none { it in text } && text.length in ALLOWED_LENGTH
9571
}
9672

97-
fun isValidTag(): Boolean = disallowedChars.none {
98-
it in tagsTextState.text
99-
} && tagsTextState.text.length in ALLOWED_LENGTH
100-
10173
fun addTag(tag: String) {
102-
tag.trim().let { newTag ->
103-
if (newTag.isNotBlank() && newTag !in addedTags.value) {
104-
addedTags.update { it + newTag }
105-
updateSuggestions("")
106-
tagsTextState.clearText()
107-
}
74+
val addedTags = state.value.addedTags
75+
val newTag = tag.trim()
76+
if (newTag.isNotBlank() && newTag !in addedTags) {
77+
updateViewState(addedTags + tag)
78+
tagsTextState.clearText()
10879
}
10980
}
11081

11182
fun removeTag(tag: String) {
112-
addedTags.update { it - tag }
113-
updateSuggestions("")
83+
updateViewState(state.value.addedTags - tag)
11484
}
11585

11686
fun removeLastTag() {
117-
addedTags.value.lastOrNull()?.let { lastTag ->
118-
removeTag(lastTag)
119-
}
87+
state.value.addedTags.lastOrNull()?.let { removeTag(it) }
12088
}
12189

122-
fun updateTags() {
123-
viewModelScope.launch {
124-
isLoading.value = true
125-
val result = if (addedTags.value.isEmpty()) {
126-
removeNodeTagsUseCase(navArgs.uuid)
127-
} else {
128-
updateNodeTagsUseCase(navArgs.uuid, addedTags.value)
129-
}
90+
fun updateTags() = viewModelScope.launch {
91+
_state.update { it.copy(isLoading = true) }
13092

131-
result
132-
.onSuccess { sendAction(AddRemoveTagsViewModelAction.Success) }
133-
.onFailure { sendAction(AddRemoveTagsViewModelAction.Failure) }
134-
.also { isLoading.value = false }
93+
if (state.value.addedTags.isEmpty()) {
94+
removeNodeTagsUseCase(navArgs.uuid)
95+
} else {
96+
updateNodeTagsUseCase(navArgs.uuid, state.value.addedTags)
13597
}
98+
.onSuccess { sendAction(AddRemoveTagsViewModelAction.Success) }
99+
.onFailure { sendAction(AddRemoveTagsViewModelAction.Failure) }
100+
.also { _state.update { it.copy(isLoading = false) } }
136101
}
137102

138-
companion object {
139-
val ALLOWED_LENGTH = 1..30
103+
fun updateViewState(addedTags: Set<String> = state.value.addedTags) {
104+
_state.update { current ->
105+
current.copy(
106+
addedTags = addedTags,
107+
suggestedTags = current.allTags
108+
.filter { it !in addedTags }
109+
.filter { it.contains(tagsTextState.text.toString(), ignoreCase = true) }
110+
.toSet(),
111+
tagsUpdated = addedTags != initialTags
112+
)
113+
}
114+
}
140115

116+
private companion object {
117+
val ALLOWED_LENGTH = 1..30
141118
const val TYPING_DEBOUNCE_TIME = 200L
142119
}
143120
}
144121

122+
data class TagsViewState(
123+
val isLoading: Boolean = false,
124+
val tagsUpdated: Boolean = false,
125+
val addedTags: Set<String> = emptySet(),
126+
val suggestedTags: Set<String> = emptySet(),
127+
val allTags: Set<String> = emptySet(),
128+
)
129+
145130
sealed interface AddRemoveTagsViewModelAction {
146131
data object Success : AddRemoveTagsViewModelAction
147132
data object Failure : AddRemoveTagsViewModelAction

0 commit comments

Comments
 (0)