Skip to content

Commit 3e5ffd6

Browse files
authored
CMM-839 odie link UI with wordpress rs (#22285)
* Creating library * Creating list screen * Adding conversation screen * Adding the + and the welcome items * Navigating to the new ai support * Renaming * Some refactor * Extracting common methods * Accessing the API * Modifying fields * Fixing message visibuil * Some styling * Extracting strings * Improving previews in code * Previewing dark mode * Removing unused func * Some styling * Extracting model classes * Some refactoring * Creating repository * Compile fix * Creating conversation * Removing userWantsToTalkToHuman * Add conversation request function * Load real data * Adding loading spinner * detekt * Username * manifest fix * compile fix * Updating rust lib version * Handling send new messages * Conversations loading spinner * Handling new messages * Loading message bubble * Preventing send message when the bot hasn't answered * Using custom OkHttp * Showing user/bot interaction * Fixing new conversation creation * Merge trunk and fixes * Using proper plurals * Theme fix * Initialization error * Basic error handling * Error handling when loading conversations * Handling empty list * Sending message error handling * Removing last message when error * Better handling the can send * Adding pull to refresh * detekt and style * Check fix * Small suggested changes and typos * Extracting WpComApiClient construction to WpComApiClientProvider to make the repo testable * Adding tests for AIBotSupportRepository * Creating tests for AIBotSupportViewModel * Injecting the IO dispatcher instead of using static reference * Potential fix for scrllong issue * removing debug code
1 parent 1d3e75f commit 3e5ffd6

File tree

17 files changed

+1411
-201
lines changed

17 files changed

+1411
-201
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.wordpress.android.networking.restapi
2+
3+
import okhttp3.OkHttpClient
4+
import rs.wordpress.api.kotlin.WpComApiClient
5+
import rs.wordpress.api.kotlin.WpHttpClient
6+
import rs.wordpress.api.kotlin.WpRequestExecutor
7+
import uniffi.wp_api.WpAuthentication
8+
import uniffi.wp_api.WpAuthenticationProvider
9+
import java.util.concurrent.TimeUnit
10+
import javax.inject.Inject
11+
12+
private const val READ_WRITE_TIMEOUT = 60L
13+
private const val CONNECT_TIMEOUT = 30L
14+
15+
class WpComApiClientProvider @Inject constructor() {
16+
fun getWpComApiClient(accessToken: String): WpComApiClient {
17+
val okHttpClient = OkHttpClient.Builder()
18+
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
19+
.readTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
20+
.writeTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
21+
.build()
22+
23+
return WpComApiClient(
24+
requestExecutor = WpRequestExecutor(httpClient = WpHttpClient.CustomOkHttpClient(okHttpClient)),
25+
authProvider = WpAuthenticationProvider.staticWithAuth(WpAuthentication.Bearer(token = accessToken!!)
26+
)
27+
)
28+
}
29+
}

WordPress/src/main/java/org/wordpress/android/support/aibot/model/BotMessage.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ data class BotMessage(
66
val id: Long,
77
val text: String,
88
val date: Date,
9-
val userWantsToTalkToHuman: Boolean,
109
val isWrittenByUser: Boolean
1110
)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package org.wordpress.android.support.aibot.repository
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.withContext
5+
import org.wordpress.android.fluxc.utils.AppLogWrapper
6+
import org.wordpress.android.modules.IO_THREAD
7+
import org.wordpress.android.networking.restapi.WpComApiClientProvider
8+
import org.wordpress.android.support.aibot.model.BotConversation
9+
import org.wordpress.android.support.aibot.model.BotMessage
10+
import org.wordpress.android.util.AppLog
11+
import rs.wordpress.api.kotlin.WpComApiClient
12+
import rs.wordpress.api.kotlin.WpRequestResult
13+
import uniffi.wp_api.AddMessageToBotConversationParams
14+
import uniffi.wp_api.BotConversationSummary
15+
import uniffi.wp_api.CreateBotConversationParams
16+
import uniffi.wp_api.GetBotConversationParams
17+
import javax.inject.Inject
18+
import javax.inject.Named
19+
20+
private const val BOT_ID = "jetpack-chat-mobile"
21+
22+
class AIBotSupportRepository @Inject constructor(
23+
private val appLogWrapper: AppLogWrapper,
24+
private val wpComApiClientProvider: WpComApiClientProvider,
25+
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
26+
) {
27+
private var accessToken: String? = null
28+
private var userId: Long = 0
29+
30+
private val wpComApiClient: WpComApiClient by lazy {
31+
check(accessToken != null || userId != 0L) { "Repository not initialized" }
32+
wpComApiClientProvider.getWpComApiClient(accessToken!!)
33+
}
34+
35+
fun init(accessToken: String, userId: Long) {
36+
this.accessToken = accessToken
37+
this.userId = userId
38+
}
39+
40+
suspend fun loadConversations(): List<BotConversation> = withContext(ioDispatcher) {
41+
val response = wpComApiClient.request { requestBuilder ->
42+
requestBuilder.supportBots().getBotConverationList(BOT_ID)
43+
}
44+
when (response) {
45+
is WpRequestResult.Success -> {
46+
val conversations = response.response.data
47+
conversations.toBotConversations()
48+
}
49+
50+
else -> {
51+
appLogWrapper.e(AppLog.T.SUPPORT, "Error loading conversations: $response")
52+
emptyList()
53+
}
54+
}
55+
}
56+
57+
suspend fun loadConversation(chatId: Long): BotConversation? = withContext(ioDispatcher) {
58+
val response = wpComApiClient.request { requestBuilder ->
59+
requestBuilder.supportBots().getBotConversation(
60+
botId = BOT_ID,
61+
chatId = chatId.toULong(),
62+
params = GetBotConversationParams()
63+
)
64+
}
65+
when (response) {
66+
is WpRequestResult.Success -> {
67+
val conversation = response.response.data
68+
conversation.toBotConversation()
69+
}
70+
71+
else -> {
72+
appLogWrapper.e(AppLog.T.SUPPORT, "Error loading conversation $chatId: $response")
73+
null
74+
}
75+
}
76+
}
77+
78+
suspend fun createNewConversation(message: String): BotConversation? = withContext(ioDispatcher) {
79+
val response = wpComApiClient.request { requestBuilder ->
80+
requestBuilder.supportBots().createBotConversation(
81+
botId = BOT_ID,
82+
CreateBotConversationParams(
83+
message = message,
84+
userId = userId
85+
)
86+
)
87+
}
88+
89+
when (response) {
90+
is WpRequestResult.Success -> {
91+
val conversation = response.response.data
92+
conversation.toBotConversation()
93+
}
94+
95+
else -> {
96+
appLogWrapper.e(AppLog.T.SUPPORT, "Error creating new conversation $response")
97+
null
98+
}
99+
}
100+
}
101+
102+
suspend fun sendMessageToConversation(chatId: Long, message: String): BotConversation? =
103+
withContext(ioDispatcher) {
104+
val response = wpComApiClient.request { requestBuilder ->
105+
requestBuilder.supportBots().addMessageToBotConversation(
106+
botId = BOT_ID,
107+
chatId = chatId.toULong(),
108+
params = AddMessageToBotConversationParams(
109+
message = message,
110+
context = mapOf()
111+
)
112+
)
113+
}
114+
115+
when (response) {
116+
is WpRequestResult.Success -> {
117+
val conversation = response.response.data
118+
conversation.toBotConversation()
119+
}
120+
121+
else -> {
122+
appLogWrapper.e(
123+
AppLog.T.SUPPORT,
124+
"Error sending message to conversation $chatId: $response"
125+
)
126+
null
127+
}
128+
}
129+
}
130+
131+
private fun List<BotConversationSummary>.toBotConversations(): List<BotConversation> =
132+
map { it.toBotConversation() }
133+
134+
135+
private fun BotConversationSummary.toBotConversation(): BotConversation =
136+
BotConversation (
137+
id = chatId.toLong(),
138+
createdAt = createdAt,
139+
mostRecentMessageDate = lastMessage.createdAt,
140+
lastMessage = lastMessage.content,
141+
messages = listOf()
142+
)
143+
144+
private fun uniffi.wp_api.BotConversation.toBotConversation(): BotConversation =
145+
BotConversation (
146+
id = chatId.toLong(),
147+
createdAt = createdAt,
148+
mostRecentMessageDate = messages.last().createdAt,
149+
lastMessage = messages.last().content,
150+
messages = messages.map { it.toBotMessage() }
151+
)
152+
153+
private fun uniffi.wp_api.BotMessage.toBotMessage(): BotMessage =
154+
BotMessage(
155+
id = messageId.toLong(),
156+
text = content,
157+
date = createdAt,
158+
isWrittenByUser = role == "user"
159+
)
160+
}

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,24 @@ import android.content.Context
44
import android.content.Intent
55
import android.os.Build
66
import android.os.Bundle
7+
import android.view.Gravity
78
import androidx.activity.viewModels
89
import androidx.appcompat.app.AppCompatActivity
910
import androidx.compose.runtime.Composable
1011
import androidx.compose.runtime.collectAsState
1112
import androidx.compose.runtime.getValue
1213
import androidx.compose.ui.platform.ComposeView
1314
import androidx.compose.ui.platform.ViewCompositionStrategy
15+
import androidx.lifecycle.lifecycleScope
1416
import androidx.navigation.NavHostController
1517
import androidx.navigation.compose.NavHost
1618
import androidx.navigation.compose.composable
1719
import androidx.navigation.compose.rememberNavController
1820
import dagger.hilt.android.AndroidEntryPoint
21+
import kotlinx.coroutines.launch
22+
import org.wordpress.android.R
1923
import org.wordpress.android.ui.compose.theme.AppThemeM3
24+
import org.wordpress.android.util.ToastUtils
2025

2126
@AndroidEntryPoint
2227
class AIBotSupportActivity : AppCompatActivity() {
@@ -42,7 +47,24 @@ class AIBotSupportActivity : AppCompatActivity() {
4247
}
4348
}
4449
)
45-
viewModel.init(intent.getStringExtra(ACCESS_TOKEN_ID)!!)
50+
viewModel.init(
51+
accessToken = intent.getStringExtra(ACCESS_TOKEN_ID)!!,
52+
userId = intent.getLongExtra(USER_ID, 0)
53+
)
54+
55+
// Observe error messages and show them as Toast
56+
lifecycleScope.launch {
57+
viewModel.errorMessage.collect { errorType ->
58+
val errorMessage = when (errorType) {
59+
AIBotSupportViewModel.ErrorType.GENERAL -> getString(R.string.ai_bot_generic_error)
60+
null -> null
61+
}
62+
errorMessage?.let {
63+
ToastUtils.showToast(this@AIBotSupportActivity, it, ToastUtils.Duration.LONG, Gravity.CENTER)
64+
viewModel.clearError()
65+
}
66+
}
67+
}
4668
}
4769

4870
private enum class ConversationScreen {
@@ -60,28 +82,39 @@ class AIBotSupportActivity : AppCompatActivity() {
6082
startDestination = ConversationScreen.List.name
6183
) {
6284
composable(route = ConversationScreen.List.name) {
85+
val isLoadingConversations by viewModel.isLoadingConversations.collectAsState()
6386
ConversationsListScreen(
6487
conversations = viewModel.conversations,
88+
isLoading = isLoadingConversations,
6589
onConversationClick = { conversation ->
66-
viewModel.selectConversation(conversation)
90+
viewModel.onConversationSelected(conversation)
6791
navController.navigate(ConversationScreen.Detail.name)
6892
},
6993
onBackClick = { finish() },
7094
onCreateNewConversationClick = {
71-
viewModel.createNewConversation()
95+
viewModel.onNewConversationClicked()
7296
viewModel.selectedConversation.value?.let { newConversation ->
7397
navController.navigate(ConversationScreen.Detail.name)
7498
}
99+
},
100+
onRefresh = {
101+
viewModel.refreshConversations()
75102
}
76103
)
77104
}
78105

79106
composable(route = ConversationScreen.Detail.name) {
80107
val selectedConversation by viewModel.selectedConversation.collectAsState()
108+
val isLoadingConversation by viewModel.isLoadingConversation.collectAsState()
109+
val isBotTyping by viewModel.isBotTyping.collectAsState()
110+
val canSendMessage by viewModel.canSendMessage.collectAsState()
81111
selectedConversation?.let { conversation ->
82112
ConversationDetailScreen(
83113
userName = userName,
84114
conversation = conversation,
115+
isLoading = isLoadingConversation,
116+
isBotTyping = isBotTyping,
117+
canSendMessage = canSendMessage,
85118
onBackClick = { navController.navigateUp() },
86119
onSendMessage = { text ->
87120
viewModel.sendMessage(text)
@@ -95,14 +128,17 @@ class AIBotSupportActivity : AppCompatActivity() {
95128

96129
companion object {
97130
private const val ACCESS_TOKEN_ID = "arg_access_token_id"
131+
private const val USER_ID = "arg_user_id"
98132
private const val USERNAME = "arg_username"
99133
@JvmStatic
100134
fun createIntent(
101135
context: Context,
102136
accessToken: String,
137+
userId: Long,
103138
userName: String,
104139
): Intent = Intent(context, AIBotSupportActivity::class.java).apply {
105140
putExtra(ACCESS_TOKEN_ID, accessToken)
141+
putExtra(USER_ID, userId)
106142
putExtra(USERNAME, userName)
107143
}
108144
}

0 commit comments

Comments
 (0)