Skip to content

Commit e066a74

Browse files
authored
CMM-894 new support general improvements (#22320)
* Put ConversationListView in common between bots and HE * Empty and error state * Skip site capitalization * Adding a11c labels * Adding headings labels * adding accessible labels to chat bubbles * detekt * Fixing tests * PR suggestion about bot chat bubble * Fixing tests * Fixing TalkBack duplication
1 parent d8affd9 commit e066a74

19 files changed

+471
-268
lines changed

WordPress/src/main/java/org/wordpress/android/support/aibot/ui/AIBotConversationDetailScreen.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import androidx.compose.ui.Alignment
4545
import androidx.compose.ui.Modifier
4646
import androidx.compose.ui.platform.LocalResources
4747
import androidx.compose.ui.res.stringResource
48+
import androidx.compose.ui.semantics.clearAndSetSemantics
49+
import androidx.compose.ui.semantics.contentDescription
50+
import androidx.compose.ui.semantics.heading
51+
import androidx.compose.ui.semantics.semantics
4852
import androidx.compose.ui.text.font.FontWeight
4953
import androidx.compose.ui.text.input.KeyboardCapitalization
5054
import androidx.compose.ui.text.style.TextAlign
@@ -195,10 +199,17 @@ fun AIBotConversationDetailScreen(
195199

196200
@Composable
197201
private fun WelcomeHeader(userName: String) {
202+
val greeting = stringResource(R.string.ai_bot_welcome_greeting, userName)
203+
val message = stringResource(R.string.ai_bot_welcome_message)
204+
val welcomeDescription = "$greeting. $message"
205+
198206
Card(
199207
modifier = Modifier
200208
.fillMaxWidth()
201-
.padding(vertical = 8.dp),
209+
.padding(vertical = 8.dp)
210+
.clearAndSetSemantics {
211+
contentDescription = welcomeDescription
212+
},
202213
colors = CardDefaults.cardColors(
203214
containerColor = MaterialTheme.colorScheme.surface
204215
),
@@ -220,7 +231,8 @@ private fun WelcomeHeader(userName: String) {
220231
text = stringResource(R.string.ai_bot_welcome_greeting, userName),
221232
style = MaterialTheme.typography.titleLarge,
222233
fontWeight = FontWeight.Bold,
223-
color = MaterialTheme.colorScheme.primary
234+
color = MaterialTheme.colorScheme.primary,
235+
modifier = Modifier.semantics { heading() }
224236
)
225237

226238
Text(
@@ -241,6 +253,7 @@ private fun ChatInputBar(
241253
onSendClick: () -> Unit
242254
) {
243255
val canSend = messageText.isNotBlank() && canSendMessage
256+
val messageInputLabel = stringResource(R.string.ai_bot_message_input_placeholder)
244257

245258
Row(
246259
modifier = Modifier
@@ -253,8 +266,10 @@ private fun ChatInputBar(
253266
OutlinedTextField(
254267
value = messageText,
255268
onValueChange = onMessageTextChange,
256-
modifier = Modifier.weight(1f),
257-
placeholder = { Text(stringResource(R.string.ai_bot_message_input_placeholder)) },
269+
modifier = Modifier
270+
.weight(1f)
271+
.semantics { contentDescription = messageInputLabel },
272+
placeholder = { Text(messageInputLabel) },
258273
maxLines = 4,
259274
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
260275
)
@@ -278,6 +293,10 @@ private fun ChatInputBar(
278293

279294
@Composable
280295
private fun MessageBubble(message: BotMessage, resources: android.content.res.Resources) {
296+
val timestamp = formatRelativeTime(message.date, resources)
297+
val author = stringResource(if (message.isWrittenByUser) R.string.ai_bot_you else R.string.ai_bot_support_bot)
298+
val messageDescription = "$author, $timestamp. ${message.formattedText}"
299+
281300
Row(
282301
modifier = Modifier.fillMaxWidth(),
283302
horizontalArrangement = if (message.isWrittenByUser) {
@@ -303,6 +322,9 @@ private fun MessageBubble(message: BotMessage, resources: android.content.res.Re
303322
)
304323
)
305324
.padding(12.dp)
325+
.clearAndSetSemantics {
326+
contentDescription = messageDescription
327+
}
306328
) {
307329
Column {
308330
Text(
@@ -319,7 +341,7 @@ private fun MessageBubble(message: BotMessage, resources: android.content.res.Re
319341
Spacer(modifier = Modifier.height(4.dp))
320342

321343
Text(
322-
text = formatRelativeTime(message.date, resources),
344+
text = timestamp,
323345
style = MaterialTheme.typography.bodySmall,
324346
color = if (message.isWrittenByUser) {
325347
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
@@ -350,6 +372,7 @@ private fun TypingIndicatorBubble() {
350372
)
351373
)
352374
.padding(16.dp)
375+
.semantics { contentDescription = "AI Bot is typing" }
353376
) {
354377
Row(
355378
horizontalArrangement = Arrangement.spacedBy(4.dp),

WordPress/src/main/java/org/wordpress/android/support/aibot/ui/AIBotConversationsListScreen.kt

Lines changed: 29 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,14 @@ import android.content.res.Resources
55
import androidx.compose.foundation.clickable
66
import androidx.compose.foundation.layout.Column
77
import androidx.compose.foundation.layout.Row
8-
import androidx.compose.foundation.layout.fillMaxSize
98
import androidx.compose.foundation.layout.fillMaxWidth
109
import androidx.compose.foundation.layout.padding
11-
import androidx.compose.foundation.lazy.LazyColumn
12-
import androidx.compose.foundation.lazy.items
13-
import androidx.compose.material.icons.Icons
14-
import androidx.compose.material.icons.automirrored.filled.ArrowBack
15-
import androidx.compose.material.icons.filled.Edit
1610
import androidx.compose.material3.ExperimentalMaterial3Api
17-
import androidx.compose.material3.HorizontalDivider
1811
import androidx.compose.material3.Icon
19-
import androidx.compose.material3.IconButton
2012
import androidx.compose.material3.MaterialTheme
21-
import androidx.compose.material3.Scaffold
22-
import androidx.compose.material3.SnackbarHost
2313
import androidx.compose.material3.SnackbarHostState
2414
import androidx.compose.material3.Text
25-
import androidx.compose.material3.TopAppBar
26-
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
2715
import androidx.compose.runtime.Composable
28-
import androidx.compose.runtime.collectAsState
29-
import androidx.compose.runtime.getValue
3016
import androidx.compose.runtime.remember
3117
import androidx.compose.ui.Alignment
3218
import androidx.compose.ui.Modifier
@@ -37,109 +23,47 @@ import androidx.compose.ui.text.font.FontWeight
3723
import androidx.compose.ui.text.style.TextOverflow
3824
import androidx.compose.ui.tooling.preview.Preview
3925
import androidx.compose.ui.unit.dp
40-
import kotlinx.coroutines.flow.MutableStateFlow
41-
import kotlinx.coroutines.flow.StateFlow
42-
import kotlinx.coroutines.flow.asStateFlow
4326
import org.wordpress.android.R
4427
import org.wordpress.android.support.aibot.model.BotConversation
4528
import org.wordpress.android.support.aibot.util.formatRelativeTime
4629
import org.wordpress.android.support.aibot.util.generateSampleBotConversations
47-
import org.wordpress.android.support.common.ui.EmptyConversationsView
30+
import org.wordpress.android.support.common.ui.ConversationsListScreen
31+
import org.wordpress.android.support.common.ui.ConversationsSupportViewModel
4832
import org.wordpress.android.ui.compose.theme.AppThemeM3
4933

5034
@OptIn(ExperimentalMaterial3Api::class)
5135
@Composable
5236
fun AIBotConversationsListScreen(
5337
snackbarHostState: SnackbarHostState,
54-
conversations: StateFlow<List<BotConversation>>,
55-
isLoading: Boolean,
38+
conversations: List<BotConversation>,
39+
conversationsState: ConversationsSupportViewModel.ConversationsState,
5640
onConversationClick: (BotConversation) -> Unit,
5741
onBackClick: () -> Unit,
5842
onCreateNewConversationClick: () -> Unit,
5943
onRefresh: () -> Unit,
6044
) {
61-
Scaffold(
62-
snackbarHost = { SnackbarHost(snackbarHostState) },
63-
topBar = {
64-
TopAppBar(
65-
title = { Text(stringResource(R.string.ai_bot_conversations_title)) },
66-
navigationIcon = {
67-
IconButton(onClick = onBackClick) {
68-
Icon(
69-
Icons.AutoMirrored.Filled.ArrowBack,
70-
stringResource(R.string.ai_bot_back_button_content_description)
71-
)
72-
}
73-
},
74-
actions = {
75-
IconButton(onClick = { onCreateNewConversationClick() }) {
76-
Icon(
77-
imageVector = Icons.Default.Edit,
78-
contentDescription = stringResource(R.string.ai_bot_new_conversation_content_description)
79-
)
80-
}
81-
}
82-
)
83-
},
84-
) { contentPadding ->
85-
val conversationsList by conversations.collectAsState()
86-
87-
PullToRefreshBox(
88-
isRefreshing = isLoading,
89-
onRefresh = onRefresh,
90-
modifier = Modifier
91-
.fillMaxSize()
92-
.padding(contentPadding)
93-
) {
94-
when {
95-
conversationsList.isEmpty() && !isLoading -> {
96-
EmptyConversationsView(
97-
modifier = Modifier.fillMaxSize(),
98-
onCreateNewConversationClick = onCreateNewConversationClick
99-
)
100-
}
101-
else -> {
102-
ShowConversationsList(
103-
modifier = Modifier.fillMaxSize(),
104-
conversations = conversations,
105-
onConversationClick = onConversationClick
106-
)
107-
}
108-
}
109-
}
110-
}
111-
}
112-
113-
@Composable
114-
private fun ShowConversationsList(
115-
modifier: Modifier,
116-
conversations: StateFlow<List<BotConversation>>,
117-
onConversationClick: (BotConversation) -> Unit
118-
) {
119-
val conversations by conversations.collectAsState()
12045
val resources = LocalResources.current
121-
122-
LazyColumn(
123-
modifier = modifier.fillMaxSize()
124-
) {
125-
items(
126-
items = conversations,
127-
key = { it.id }
128-
) { conversation ->
129-
ConversationListItem(
46+
ConversationsListScreen(
47+
title = stringResource(R.string.ai_bot_conversations_title),
48+
addConversationContentDescription = stringResource(R.string.ai_bot_new_conversation_content_description),
49+
snackbarHostState = snackbarHostState,
50+
conversations = conversations,
51+
conversationsState = conversationsState,
52+
onBackClick = onBackClick,
53+
onCreateNewConversationClick = onCreateNewConversationClick,
54+
onRefresh = onRefresh,
55+
conversationListItem = { conversation ->
56+
BotConversationListItem(
13057
conversation = conversation,
13158
resources = resources,
13259
onClick = { onConversationClick(conversation) }
13360
)
134-
HorizontalDivider(
135-
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
136-
)
13761
}
138-
}
62+
)
13963
}
14064

14165
@Composable
142-
private fun ConversationListItem(
66+
private fun BotConversationListItem(
14367
conversation: BotConversation,
14468
resources: Resources,
14569
onClick: () -> Unit
@@ -183,14 +107,13 @@ private fun ConversationListItem(
183107
@Preview(showBackground = true, name = "Conversations List")
184108
@Composable
185109
private fun ConversationsScreenPreview() {
186-
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
187110
val snackbarHostState = remember { SnackbarHostState() }
188111

189112
AppThemeM3(isDarkTheme = false) {
190113
AIBotConversationsListScreen(
191114
snackbarHostState = snackbarHostState,
192-
conversations = sampleConversations.asStateFlow(),
193-
isLoading = false,
115+
conversations = generateSampleBotConversations(),
116+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
194117
onConversationClick = { },
195118
onBackClick = { },
196119
onCreateNewConversationClick = { },
@@ -202,14 +125,13 @@ private fun ConversationsScreenPreview() {
202125
@Preview(showBackground = true, name = "Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
203126
@Composable
204127
private fun ConversationsScreenPreviewDark() {
205-
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
206128
val snackbarHostState = remember { SnackbarHostState() }
207129

208130
AppThemeM3(isDarkTheme = true) {
209131
AIBotConversationsListScreen(
210132
snackbarHostState = snackbarHostState,
211-
conversations = sampleConversations.asStateFlow(),
212-
isLoading = false,
133+
conversations = generateSampleBotConversations(),
134+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
213135
onConversationClick = { },
214136
onBackClick = { },
215137
onCreateNewConversationClick = { },
@@ -221,14 +143,13 @@ private fun ConversationsScreenPreviewDark() {
221143
@Preview(showBackground = true, name = "Conversations List")
222144
@Composable
223145
private fun ConversationsScreenWordPressPreview() {
224-
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
225146
val snackbarHostState = remember { SnackbarHostState() }
226147

227148
AppThemeM3(isDarkTheme = false, isJetpackApp = false) {
228149
AIBotConversationsListScreen(
229150
snackbarHostState = snackbarHostState,
230-
conversations = sampleConversations.asStateFlow(),
231-
isLoading = true,
151+
conversations = generateSampleBotConversations(),
152+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
232153
onConversationClick = { },
233154
onBackClick = { },
234155
onCreateNewConversationClick = { },
@@ -240,14 +161,13 @@ private fun ConversationsScreenWordPressPreview() {
240161
@Preview(showBackground = true, name = "Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
241162
@Composable
242163
private fun ConversationsScreenPreviewWordPressDark() {
243-
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
244164
val snackbarHostState = remember { SnackbarHostState() }
245165

246166
AppThemeM3(isDarkTheme = true, isJetpackApp = false) {
247167
AIBotConversationsListScreen(
248168
snackbarHostState = snackbarHostState,
249-
conversations = sampleConversations.asStateFlow(),
250-
isLoading = true,
169+
conversations = generateSampleBotConversations(),
170+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
251171
onConversationClick = { },
252172
onBackClick = { },
253173
onCreateNewConversationClick = { },
@@ -259,14 +179,13 @@ private fun ConversationsScreenPreviewWordPressDark() {
259179
@Preview(showBackground = true, name = "Empty Conversations List")
260180
@Composable
261181
private fun EmptyConversationsScreenPreview() {
262-
val emptyConversations = MutableStateFlow(emptyList<BotConversation>())
263182
val snackbarHostState = remember { SnackbarHostState() }
264183

265184
AppThemeM3(isDarkTheme = false) {
266185
AIBotConversationsListScreen(
267186
snackbarHostState = snackbarHostState,
268-
conversations = emptyConversations.asStateFlow(),
269-
isLoading = false,
187+
conversations = emptyList(),
188+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
270189
onConversationClick = { },
271190
onBackClick = { },
272191
onCreateNewConversationClick = { },
@@ -278,14 +197,13 @@ private fun EmptyConversationsScreenPreview() {
278197
@Preview(showBackground = true, name = "Empty Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
279198
@Composable
280199
private fun EmptyConversationsScreenPreviewDark() {
281-
val emptyConversations = MutableStateFlow(emptyList<BotConversation>())
282200
val snackbarHostState = remember { SnackbarHostState() }
283201

284202
AppThemeM3(isDarkTheme = true) {
285203
AIBotConversationsListScreen(
286204
snackbarHostState = snackbarHostState,
287-
conversations = emptyConversations.asStateFlow(),
288-
isLoading = false,
205+
conversations = emptyList(),
206+
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
289207
onConversationClick = { },
290208
onBackClick = { },
291209
onCreateNewConversationClick = { },

WordPress/src/main/java/org/wordpress/android/support/aibot/ui/AIBotSupportActivity.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,12 @@ class AIBotSupportActivity : AppCompatActivity() {
109109
startDestination = ConversationScreen.List.name,
110110
) {
111111
composable(route = ConversationScreen.List.name) {
112-
val isLoadingConversations by viewModel.isLoadingConversations.collectAsState()
112+
val conversationsState by viewModel.conversationsState.collectAsState()
113+
val conversations by viewModel.conversations.collectAsState()
113114
AIBotConversationsListScreen(
114115
snackbarHostState = snackbarHostState,
115-
conversations = viewModel.conversations,
116-
isLoading = isLoadingConversations,
116+
conversations = conversations,
117+
conversationsState = conversationsState,
117118
onConversationClick = { conversation ->
118119
viewModel.onConversationClick(conversation)
119120
},

WordPress/src/main/java/org/wordpress/android/support/aibot/ui/AIBotSupportViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.wordpress.android.support.aibot.repository.AIBotSupportRepository
1616
import org.wordpress.android.support.common.ui.ConversationsSupportViewModel
1717
import org.wordpress.android.ui.compose.utils.markdownToAnnotatedString
1818
import org.wordpress.android.util.AppLog
19+
import org.wordpress.android.util.NetworkUtilsWrapper
1920
import java.util.Date
2021
import javax.inject.Inject
2122

@@ -24,7 +25,8 @@ class AIBotSupportViewModel @Inject constructor(
2425
accountStore: AccountStore,
2526
private val aiBotSupportRepository: AIBotSupportRepository,
2627
appLogWrapper: AppLogWrapper,
27-
) : ConversationsSupportViewModel<BotConversation>(accountStore, appLogWrapper) {
28+
networkUtilsWrapper: NetworkUtilsWrapper,
29+
) : ConversationsSupportViewModel<BotConversation>(accountStore, appLogWrapper, networkUtilsWrapper) {
2830
private val _canSendMessage = MutableStateFlow(true)
2931
val canSendMessage: StateFlow<Boolean> = _canSendMessage.asStateFlow()
3032

0 commit comments

Comments
 (0)