Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import javax.inject.Inject
import javax.inject.Named

private const val BOT_ID = "jetpack-chat-mobile"
private const val ITEMS_PER_PAGE = 20

class AIBotSupportRepository @Inject constructor(
private val appLogWrapper: AppLogWrapper,
Expand Down Expand Up @@ -75,9 +76,7 @@ class AIBotSupportRepository @Inject constructor(
chatId = chatId.toULong(),
params = GetBotConversationParams(
pageNumber = pageNumber.toULong(),
// TODO: this is set to 4 for testing purpose
// The TODO is preventing the Pr to be merged. Change it to a higher number before that
itemsPerPage = 4U
itemsPerPage = ITEMS_PER_PAGE.toULong()
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
Expand Down Expand Up @@ -195,10 +199,17 @@ fun AIBotConversationDetailScreen(

@Composable
private fun WelcomeHeader(userName: String) {
val greeting = stringResource(R.string.ai_bot_welcome_greeting, userName)
val message = stringResource(R.string.ai_bot_welcome_message)
val welcomeDescription = "$greeting. $message"

Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
.padding(vertical = 8.dp)
.clearAndSetSemantics {
contentDescription = welcomeDescription
},
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
Expand All @@ -220,7 +231,8 @@ private fun WelcomeHeader(userName: String) {
text = stringResource(R.string.ai_bot_welcome_greeting, userName),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.semantics { heading() }
)

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

Row(
modifier = Modifier
Expand All @@ -253,8 +266,10 @@ private fun ChatInputBar(
OutlinedTextField(
value = messageText,
onValueChange = onMessageTextChange,
modifier = Modifier.weight(1f),
placeholder = { Text(stringResource(R.string.ai_bot_message_input_placeholder)) },
modifier = Modifier
.weight(1f)
.semantics { contentDescription = messageInputLabel },
placeholder = { Text(messageInputLabel) },
maxLines = 4,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
)
Expand All @@ -278,6 +293,10 @@ private fun ChatInputBar(

@Composable
private fun MessageBubble(message: BotMessage, resources: android.content.res.Resources) {
val timestamp = formatRelativeTime(message.date, resources)
val author = stringResource(if (message.isWrittenByUser) R.string.ai_bot_you else R.string.ai_bot_support_bot)
val messageDescription = "$author, $timestamp. ${message.formattedText}"

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isWrittenByUser) {
Expand All @@ -303,6 +322,9 @@ private fun MessageBubble(message: BotMessage, resources: android.content.res.Re
)
)
.padding(12.dp)
.clearAndSetSemantics {
contentDescription = messageDescription
}
) {
Column {
Text(
Expand All @@ -318,7 +340,7 @@ private fun MessageBubble(message: BotMessage, resources: android.content.res.Re
Spacer(modifier = Modifier.height(4.dp))

Text(
text = formatRelativeTime(message.date, resources),
text = timestamp,
style = MaterialTheme.typography.bodySmall,
color = if (message.isWrittenByUser) {
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
Expand Down Expand Up @@ -349,6 +371,7 @@ private fun TypingIndicatorBubble() {
)
)
.padding(16.dp)
.semantics { contentDescription = "AI Bot is typing" }
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,14 @@ import android.content.res.Resources
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -37,109 +23,47 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.wordpress.android.R
import org.wordpress.android.support.aibot.model.BotConversation
import org.wordpress.android.support.aibot.util.formatRelativeTime
import org.wordpress.android.support.aibot.util.generateSampleBotConversations
import org.wordpress.android.support.common.ui.EmptyConversationsView
import org.wordpress.android.support.common.ui.ConversationsListScreen
import org.wordpress.android.support.common.ui.ConversationsSupportViewModel
import org.wordpress.android.ui.compose.theme.AppThemeM3

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AIBotConversationsListScreen(
snackbarHostState: SnackbarHostState,
conversations: StateFlow<List<BotConversation>>,
isLoading: Boolean,
conversations: List<BotConversation>,
conversationsState: ConversationsSupportViewModel.ConversationsState,
onConversationClick: (BotConversation) -> Unit,
onBackClick: () -> Unit,
onCreateNewConversationClick: () -> Unit,
onRefresh: () -> Unit,
) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.ai_bot_conversations_title)) },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
stringResource(R.string.ai_bot_back_button_content_description)
)
}
},
actions = {
IconButton(onClick = { onCreateNewConversationClick() }) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = stringResource(R.string.ai_bot_new_conversation_content_description)
)
}
}
)
},
) { contentPadding ->
val conversationsList by conversations.collectAsState()

PullToRefreshBox(
isRefreshing = isLoading,
onRefresh = onRefresh,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
when {
conversationsList.isEmpty() && !isLoading -> {
EmptyConversationsView(
modifier = Modifier.fillMaxSize(),
onCreateNewConversationClick = onCreateNewConversationClick
)
}
else -> {
ShowConversationsList(
modifier = Modifier.fillMaxSize(),
conversations = conversations,
onConversationClick = onConversationClick
)
}
}
}
}
}

@Composable
private fun ShowConversationsList(
modifier: Modifier,
conversations: StateFlow<List<BotConversation>>,
onConversationClick: (BotConversation) -> Unit
) {
val conversations by conversations.collectAsState()
val resources = LocalResources.current

LazyColumn(
modifier = modifier.fillMaxSize()
) {
items(
items = conversations,
key = { it.id }
) { conversation ->
ConversationListItem(
ConversationsListScreen(
title = stringResource(R.string.ai_bot_conversations_title),
addConversationContentDescription = stringResource(R.string.ai_bot_new_conversation_content_description),
snackbarHostState = snackbarHostState,
conversations = conversations,
conversationsState = conversationsState,
onBackClick = onBackClick,
onCreateNewConversationClick = onCreateNewConversationClick,
onRefresh = onRefresh,
conversationListItem = { conversation ->
BotConversationListItem(
conversation = conversation,
resources = resources,
onClick = { onConversationClick(conversation) }
)
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
)
}
}
)
}

@Composable
private fun ConversationListItem(
private fun BotConversationListItem(
conversation: BotConversation,
resources: Resources,
onClick: () -> Unit
Expand Down Expand Up @@ -183,14 +107,13 @@ private fun ConversationListItem(
@Preview(showBackground = true, name = "Conversations List")
@Composable
private fun ConversationsScreenPreview() {
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = false) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = sampleConversations.asStateFlow(),
isLoading = false,
conversations = generateSampleBotConversations(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand All @@ -202,14 +125,13 @@ private fun ConversationsScreenPreview() {
@Preview(showBackground = true, name = "Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun ConversationsScreenPreviewDark() {
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = true) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = sampleConversations.asStateFlow(),
isLoading = false,
conversations = generateSampleBotConversations(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand All @@ -221,14 +143,13 @@ private fun ConversationsScreenPreviewDark() {
@Preview(showBackground = true, name = "Conversations List")
@Composable
private fun ConversationsScreenWordPressPreview() {
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = false, isJetpackApp = false) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = sampleConversations.asStateFlow(),
isLoading = true,
conversations = generateSampleBotConversations(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand All @@ -240,14 +161,13 @@ private fun ConversationsScreenWordPressPreview() {
@Preview(showBackground = true, name = "Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun ConversationsScreenPreviewWordPressDark() {
val sampleConversations = MutableStateFlow(generateSampleBotConversations())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = true, isJetpackApp = false) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = sampleConversations.asStateFlow(),
isLoading = true,
conversations = generateSampleBotConversations(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand All @@ -259,14 +179,13 @@ private fun ConversationsScreenPreviewWordPressDark() {
@Preview(showBackground = true, name = "Empty Conversations List")
@Composable
private fun EmptyConversationsScreenPreview() {
val emptyConversations = MutableStateFlow(emptyList<BotConversation>())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = false) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = emptyConversations.asStateFlow(),
isLoading = false,
conversations = emptyList(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand All @@ -278,14 +197,13 @@ private fun EmptyConversationsScreenPreview() {
@Preview(showBackground = true, name = "Empty Conversations List - Dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun EmptyConversationsScreenPreviewDark() {
val emptyConversations = MutableStateFlow(emptyList<BotConversation>())
val snackbarHostState = remember { SnackbarHostState() }

AppThemeM3(isDarkTheme = true) {
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = emptyConversations.asStateFlow(),
isLoading = false,
conversations = emptyList(),
conversationsState = ConversationsSupportViewModel.ConversationsState.Loaded,
onConversationClick = { },
onBackClick = { },
onCreateNewConversationClick = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,12 @@ class AIBotSupportActivity : AppCompatActivity() {
startDestination = ConversationScreen.List.name,
) {
composable(route = ConversationScreen.List.name) {
val isLoadingConversations by viewModel.isLoadingConversations.collectAsState()
val conversationsState by viewModel.conversationsState.collectAsState()
val conversations by viewModel.conversations.collectAsState()
AIBotConversationsListScreen(
snackbarHostState = snackbarHostState,
conversations = viewModel.conversations,
isLoading = isLoadingConversations,
conversations = conversations,
conversationsState = conversationsState,
onConversationClick = { conversation ->
viewModel.onConversationClick(conversation)
},
Expand Down
Loading