Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
ee9bde8
Creating library
adalpari Oct 13, 2025
df7aa44
Creating list screen
adalpari Oct 13, 2025
488c373
Adding conversation screen
adalpari Oct 13, 2025
1fee31d
Adding the + and the welcome items
adalpari Oct 13, 2025
036fec7
Navigating to the new ai support
adalpari Oct 13, 2025
817feb0
Renaming
adalpari Oct 13, 2025
b74f1ab
Some refactor
adalpari Oct 13, 2025
77423c4
Extracting common methods
adalpari Oct 13, 2025
5ac7784
Accessing the API
adalpari Oct 13, 2025
6324b04
Modifying fields
adalpari Oct 13, 2025
d677ce0
Fixing message visibuil
adalpari Oct 13, 2025
e14fe06
Some styling
adalpari Oct 13, 2025
d804184
Extracting strings
adalpari Oct 13, 2025
82f1da0
Improving previews in code
adalpari Oct 13, 2025
54e8621
Previewing dark mode
adalpari Oct 13, 2025
2b6c494
Removing unused func
adalpari Oct 13, 2025
959072d
Some styling
adalpari Oct 13, 2025
0195829
Extracting model classes
adalpari Oct 14, 2025
368ea9a
Some refactoring
adalpari Oct 14, 2025
8ecf787
Creating repository
adalpari Oct 14, 2025
a473461
Compile fix
adalpari Oct 14, 2025
e33a47a
Creating conversation
adalpari Oct 14, 2025
15cb4b5
Removing userWantsToTalkToHuman
adalpari Oct 14, 2025
9b7a67b
Add conversation request function
adalpari Oct 14, 2025
be0347f
Load real data
adalpari Oct 14, 2025
e4b81f6
Adding loading spinner
adalpari Oct 14, 2025
53463e8
detekt
adalpari Oct 14, 2025
f17a90f
Username
adalpari Oct 14, 2025
e80af2d
manifest fix
adalpari Oct 14, 2025
8cb5d72
Merge branch 'trunk' into feature/CMM-837-Oddie-bot-support-UI
adalpari Oct 14, 2025
1ff9448
'feature/CMM-837-Oddie-bot-support-UI' into feature/CMM-839-Odie-lin…
adalpari Oct 14, 2025
1687538
compile fix
adalpari Oct 14, 2025
775794f
Updating rust lib version
adalpari Oct 14, 2025
24c91f3
Handling send new messages
adalpari Oct 14, 2025
4bdb8a5
Conversations loading spinner
adalpari Oct 14, 2025
b84074d
Handling new messages
adalpari Oct 14, 2025
f580498
Loading message bubble
adalpari Oct 14, 2025
89ecd6f
Preventing send message when the bot hasn't answered
adalpari Oct 14, 2025
45d0117
Using custom OkHttp
adalpari Oct 14, 2025
180b985
Showing user/bot interaction
adalpari Oct 14, 2025
da2150a
Fixing new conversation creation
adalpari Oct 14, 2025
7810fad
Merge branch 'trunk' into feature/CMM-839-Odie-link-UI-with-wordpress-rs
adalpari Oct 15, 2025
c9edfe2
Merge trunk and fixes
adalpari Oct 15, 2025
59d6c50
Using proper plurals
adalpari Oct 15, 2025
114c1f5
Theme fix
adalpari Oct 15, 2025
910aac9
Initialization error
adalpari Oct 15, 2025
e2489a6
Basic error handling
adalpari Oct 15, 2025
4284bbb
Error handling when loading conversations
adalpari Oct 15, 2025
69bbe25
Handling empty list
adalpari Oct 15, 2025
0546031
Sending message error handling
adalpari Oct 15, 2025
865bbd3
Removing last message when error
adalpari Oct 15, 2025
ea53ca7
Better handling the can send
adalpari Oct 15, 2025
5b4e4db
Adding pull to refresh
adalpari Oct 15, 2025
b729a0a
detekt and style
adalpari Oct 15, 2025
c0cffe1
Check fix
adalpari Oct 15, 2025
dbbf5e8
Small suggested changes and typos
adalpari Oct 15, 2025
6617f08
Extracting WpComApiClient construction to WpComApiClientProvider to m…
adalpari Oct 15, 2025
fe7ff77
Adding tests for AIBotSupportRepository
adalpari Oct 15, 2025
6358099
Creating tests for AIBotSupportViewModel
adalpari Oct 15, 2025
fc3eb5b
Injecting the IO dispatcher instead of using static reference
adalpari Oct 15, 2025
a1d6f81
Merge branch 'trunk' into feature/CMM-839-Odie-link-UI-with-wordpress-rs
adalpari Oct 17, 2025
a22e42e
Potential fix for scrllong issue
adalpari Oct 17, 2025
9a2e2a0
Merge branch 'feature/CMM-839-Odie-link-UI-with-wordpress-rs' of http…
adalpari Oct 17, 2025
aaecb01
Merge branch 'trunk' into feature/CMM-839-Odie-link-UI-with-wordpress-rs
adalpari Oct 20, 2025
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
@@ -0,0 +1,29 @@
package org.wordpress.android.networking.restapi

import okhttp3.OkHttpClient
import rs.wordpress.api.kotlin.WpComApiClient
import rs.wordpress.api.kotlin.WpHttpClient
import rs.wordpress.api.kotlin.WpRequestExecutor
import uniffi.wp_api.WpAuthentication
import uniffi.wp_api.WpAuthenticationProvider
import java.util.concurrent.TimeUnit
import javax.inject.Inject

private const val READ_WRITE_TIMEOUT = 60L
private const val CONNECT_TIMEOUT = 30L

class WpComApiClientProvider @Inject constructor() {
fun getWpComApiClient(accessToken: String): WpComApiClient {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.build()

return WpComApiClient(
requestExecutor = WpRequestExecutor(httpClient = WpHttpClient.CustomOkHttpClient(okHttpClient)),
authProvider = WpAuthenticationProvider.staticWithAuth(WpAuthentication.Bearer(token = accessToken!!)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ data class BotMessage(
val id: Long,
val text: String,
val date: Date,
val userWantsToTalkToHuman: Boolean,
val isWrittenByUser: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package org.wordpress.android.support.aibot.repository

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.modules.IO_THREAD
import org.wordpress.android.networking.restapi.WpComApiClientProvider
import org.wordpress.android.support.aibot.model.BotConversation
import org.wordpress.android.support.aibot.model.BotMessage
import org.wordpress.android.util.AppLog
import rs.wordpress.api.kotlin.WpComApiClient
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.AddMessageToBotConversationParams
import uniffi.wp_api.BotConversationSummary
import uniffi.wp_api.CreateBotConversationParams
import uniffi.wp_api.GetBotConversationParams
import javax.inject.Inject
import javax.inject.Named

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

class AIBotSupportRepository @Inject constructor(
private val appLogWrapper: AppLogWrapper,
private val wpComApiClientProvider: WpComApiClientProvider,
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
) {
private var accessToken: String? = null
private var userId: Long = 0

private val wpComApiClient: WpComApiClient by lazy {
check(accessToken != null || userId != 0L) { "Repository not initialized" }
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both accessToken and userId are required for the client; the OR condition allows initialization with only one set, leading to a possible NPE on accessToken!!. Change to logical AND: check(accessToken != null && userId != 0L) { "Repository not initialized" }.

Suggested change
check(accessToken != null || userId != 0L) { "Repository not initialized" }
check(accessToken != null && userId != 0L) { "Repository not initialized" }

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emmm, that's not the way it works... It wull fail if "condition 1 OR condition 2" fail. So, it looks correct to me...

wpComApiClientProvider.getWpComApiClient(accessToken!!)
}

fun init(accessToken: String, userId: Long) {
this.accessToken = accessToken
this.userId = userId
}

suspend fun loadConversations(): List<BotConversation> = withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().getBotConverationList(BOT_ID)
}
when (response) {
is WpRequestResult.Success -> {
val conversations = response.response.data
conversations.toBotConversations()
}

else -> {
appLogWrapper.e(AppLog.T.SUPPORT, "Error loading conversations: $response")
emptyList()
}
}
}

suspend fun loadConversation(chatId: Long): BotConversation? = withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().getBotConversation(
botId = BOT_ID,
chatId = chatId.toULong(),
params = GetBotConversationParams()
)
}
when (response) {
is WpRequestResult.Success -> {
val conversation = response.response.data
conversation.toBotConversation()
}

else -> {
appLogWrapper.e(AppLog.T.SUPPORT, "Error loading conversation $chatId: $response")
null
}
}
}

suspend fun createNewConversation(message: String): BotConversation? = withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().createBotConversation(
botId = BOT_ID,
CreateBotConversationParams(
message = message,
userId = userId
)
)
}

when (response) {
is WpRequestResult.Success -> {
val conversation = response.response.data
conversation.toBotConversation()
}

else -> {
appLogWrapper.e(AppLog.T.SUPPORT, "Error creating new conversation $response")
null
}
}
}

suspend fun sendMessageToConversation(chatId: Long, message: String): BotConversation? =
withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().addMessageToBotConversation(
botId = BOT_ID,
chatId = chatId.toULong(),
params = AddMessageToBotConversationParams(
message = message,
context = mapOf()
)
)
}

when (response) {
is WpRequestResult.Success -> {
val conversation = response.response.data
conversation.toBotConversation()
}

else -> {
appLogWrapper.e(
AppLog.T.SUPPORT,
"Error sending message to conversation $chatId: $response"
)
null
}
}
}

private fun List<BotConversationSummary>.toBotConversations(): List<BotConversation> =
map { it.toBotConversation() }


private fun BotConversationSummary.toBotConversation(): BotConversation =
BotConversation (
id = chatId.toLong(),
createdAt = createdAt,
mostRecentMessageDate = lastMessage.createdAt,
lastMessage = lastMessage.content,
messages = listOf()
)

private fun uniffi.wp_api.BotConversation.toBotConversation(): BotConversation =
BotConversation (
id = chatId.toLong(),
createdAt = createdAt,
mostRecentMessageDate = messages.last().createdAt,
lastMessage = messages.last().content,
messages = messages.map { it.toBotMessage() }
)

private fun uniffi.wp_api.BotMessage.toBotMessage(): BotMessage =
BotMessage(
id = messageId.toLong(),
text = content,
date = createdAt,
isWrittenByUser = role == "user"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.util.ToastUtils

@AndroidEntryPoint
class AIBotSupportActivity : AppCompatActivity() {
Expand All @@ -42,7 +47,24 @@ class AIBotSupportActivity : AppCompatActivity() {
}
}
)
viewModel.init(intent.getStringExtra(ACCESS_TOKEN_ID)!!)
viewModel.init(
accessToken = intent.getStringExtra(ACCESS_TOKEN_ID)!!,
userId = intent.getLongExtra(USER_ID, 0)
)

// Observe error messages and show them as Toast
lifecycleScope.launch {
viewModel.errorMessage.collect { errorType ->
val errorMessage = when (errorType) {
AIBotSupportViewModel.ErrorType.GENERAL -> getString(R.string.ai_bot_generic_error)
null -> null
}
errorMessage?.let {
ToastUtils.showToast(this@AIBotSupportActivity, it, ToastUtils.Duration.LONG, Gravity.CENTER)
viewModel.clearError()
}
}
}
}

private enum class ConversationScreen {
Expand All @@ -60,28 +82,39 @@ class AIBotSupportActivity : AppCompatActivity() {
startDestination = ConversationScreen.List.name
) {
composable(route = ConversationScreen.List.name) {
val isLoadingConversations by viewModel.isLoadingConversations.collectAsState()
ConversationsListScreen(
conversations = viewModel.conversations,
isLoading = isLoadingConversations,
onConversationClick = { conversation ->
viewModel.selectConversation(conversation)
viewModel.onConversationSelected(conversation)
navController.navigate(ConversationScreen.Detail.name)
},
onBackClick = { finish() },
onCreateNewConversationClick = {
viewModel.createNewConversation()
viewModel.onNewConversationClicked()
viewModel.selectedConversation.value?.let { newConversation ->
navController.navigate(ConversationScreen.Detail.name)
}
},
onRefresh = {
viewModel.refreshConversations()
}
)
}

composable(route = ConversationScreen.Detail.name) {
val selectedConversation by viewModel.selectedConversation.collectAsState()
val isLoadingConversation by viewModel.isLoadingConversation.collectAsState()
val isBotTyping by viewModel.isBotTyping.collectAsState()
val canSendMessage by viewModel.canSendMessage.collectAsState()
selectedConversation?.let { conversation ->
ConversationDetailScreen(
userName = userName,
conversation = conversation,
isLoading = isLoadingConversation,
isBotTyping = isBotTyping,
canSendMessage = canSendMessage,
onBackClick = { navController.navigateUp() },
onSendMessage = { text ->
viewModel.sendMessage(text)
Expand All @@ -95,14 +128,17 @@ class AIBotSupportActivity : AppCompatActivity() {

companion object {
private const val ACCESS_TOKEN_ID = "arg_access_token_id"
private const val USER_ID = "arg_user_id"
private const val USERNAME = "arg_username"
@JvmStatic
fun createIntent(
context: Context,
accessToken: String,
userId: Long,
userName: String,
): Intent = Intent(context, AIBotSupportActivity::class.java).apply {
putExtra(ACCESS_TOKEN_ID, accessToken)
putExtra(USER_ID, userId)
putExtra(USERNAME, userName)
}
}
Expand Down
Loading