Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
168 commits
Select commit Hold shift + click to select a range
80d4ed3
Adding basic UI
adalpari Oct 17, 2025
4836d41
Renaming
adalpari Oct 17, 2025
37541d0
Some styling
adalpari Oct 17, 2025
51b01a7
Renaming and dummy data
adalpari Oct 17, 2025
6c83ae3
Using proper "new conversation icon"
adalpari Oct 17, 2025
8d7ea50
Conversation details screen
adalpari Oct 17, 2025
a0a146b
Creating the reply bottomsheet
adalpari Oct 17, 2025
eebc0ab
Linking to the support screen
adalpari Oct 17, 2025
37676c8
bottomsheet fix
adalpari Oct 17, 2025
703f4c4
Mov navigation form activity to viewmodel
adalpari Oct 20, 2025
f6be7fd
Adding create ticket screen
adalpari Oct 20, 2025
d345864
More screen adjustments
adalpari Oct 20, 2025
05773ec
Extracting common code
adalpari Oct 20, 2025
b442787
Margin fix
adalpari Oct 20, 2025
cf4762e
detekt
adalpari Oct 20, 2025
7d318b1
Style
adalpari Oct 20, 2025
1ab8f3b
Merge branch 'trunk' into feat/CMM-843-Create-the-Ask-the-HE-entry-UI
adalpari Oct 20, 2025
d585a4a
New ticket check
adalpari Oct 20, 2025
8c651fc
Creating tests
adalpari Oct 20, 2025
fdf926b
Merge branch 'feat/CMM-843-Create-the-Ask-the-HE-entry-UI' of https:/…
adalpari Oct 20, 2025
3515fd0
Creating repository and load conversations function
adalpari Oct 21, 2025
3d99919
Adding createConversation function
adalpari Oct 21, 2025
0be28b4
Creating loadConversation func
adalpari Oct 21, 2025
40a5880
Loading conversations form the viewmodel
adalpari Oct 21, 2025
a55994e
Adding loading spinner
adalpari Oct 21, 2025
c82458c
Pull to refresh
adalpari Oct 21, 2025
73a434a
Proper ionitialization
adalpari Oct 21, 2025
087d07a
Adding empty screen
adalpari Oct 21, 2025
3b5a1e5
Handling send new conversation
adalpari Oct 21, 2025
4febc3d
Show loading when sending
adalpari Oct 21, 2025
e1215a9
New ticket creation fix
adalpari Oct 21, 2025
98cbb1f
Using snackbar for errors
adalpari Oct 21, 2025
5d421d3
Error handling
adalpari Oct 21, 2025
af8e1dd
Answering conversation
adalpari Oct 21, 2025
cad1eec
Adding some test to the repository
adalpari Oct 21, 2025
481ae11
More tests!
adalpari Oct 21, 2025
4e0b242
Merge branch 'trunk' into feat/CMM-872-support-HE-conversations-and-t…
adalpari Oct 22, 2025
95c80bd
Compile fixes
adalpari Oct 22, 2025
d36882e
Similarities improvements
adalpari Oct 22, 2025
563f58b
Using snackbar in bots activity
adalpari Oct 22, 2025
ab8ca28
Extracting EmptyConversationsView
adalpari Oct 22, 2025
034288e
Renaming
adalpari Oct 22, 2025
049df3e
Extracting VM and UI common code
adalpari Oct 22, 2025
15ab84e
Extracting navigation common code
adalpari Oct 22, 2025
53085d0
Renaming VMs for clarification
adalpari Oct 22, 2025
8c057bf
More refactor
adalpari Oct 22, 2025
a4ed792
Capitalise text fields
adalpari Oct 22, 2025
ccef4b7
Updating rs library
adalpari Oct 22, 2025
be6a5f2
Loading conversation UX
adalpari Oct 22, 2025
f023bf8
Style fix
adalpari Oct 22, 2025
d33e512
Fixing scaffolds paddings
adalpari Oct 22, 2025
ca5af7a
userID fix
adalpari Oct 22, 2025
972641d
Fixing the padding problem in bot chat when the keyboard is opened
adalpari Oct 22, 2025
d328103
Apply padding to create ticket screen when the keyboard is opened
adalpari Oct 22, 2025
4501d9a
Fixing scroll state in reply bottomsheet
adalpari Oct 22, 2025
c2baa18
Adding tests for the new common viewmodel
adalpari Oct 22, 2025
6aa8de1
Fixing AIBotSupportViewModel tests
adalpari Oct 22, 2025
204afef
detekt
adalpari Oct 22, 2025
a304d67
Improvements int he conversation interaction
adalpari Oct 22, 2025
6ee853b
Adding tests for HE VM
adalpari Oct 22, 2025
d0a549c
Merge branch 'trunk' into feat/CMM-872-support-HE-conversations-and-t…
adalpari Oct 22, 2025
bc476a7
Merge remote-tracking branch 'origin/trunk' into feat/CMM-872-support…
adalpari Oct 23, 2025
a13ae5b
Saving draft state
adalpari Oct 23, 2025
49f1af3
Properly navigating when a ticket is selected
adalpari Oct 23, 2025
e394c7a
Error parsing improvement
adalpari Oct 23, 2025
a6c421c
accessToken suggestion improvements
adalpari Oct 23, 2025
dbcb453
General suggestions
adalpari Oct 23, 2025
03f00fe
Send message error UX improvement
adalpari Oct 23, 2025
c117fcf
Fixing tests
adalpari Oct 23, 2025
d40d1d2
Converting the UI to more AndroidMaterial style
adalpari Oct 23, 2025
d318c2d
Bots screen renaming
adalpari Oct 23, 2025
c12f0fd
Bots screens renaming
adalpari Oct 23, 2025
1466115
Merge branch 'feat/CMM-872-support-HE-conversations-and-tickets-logic…
adalpari Oct 23, 2025
1d4a490
Make NewTicket screen more Android Material theme as well
adalpari Oct 24, 2025
7232fb2
Adding preview for EmptyConversationsView
adalpari Oct 24, 2025
a6e3e65
Button fix
adalpari Oct 24, 2025
19fcdf6
detekt
adalpari Oct 24, 2025
c41d801
Merge branch 'trunk' into feat/CMM-884-support-Iterate-over-the-whole…
adalpari Oct 27, 2025
6ddffcf
Ticket selection change
adalpari Oct 27, 2025
30fb83f
Supporting markdown text
adalpari Oct 27, 2025
3f908f2
detekt
adalpari Oct 27, 2025
1f6f555
Improving MarkdownUtils
adalpari Oct 27, 2025
727644c
Formatting text in the repository layer instead the ui
adalpari Oct 27, 2025
3eb939c
Renaming
adalpari Oct 27, 2025
455c100
Fixing tests
adalpari Oct 27, 2025
a07d01c
Support pagination
adalpari Oct 27, 2025
b8fe4af
Triggering in the 4th element
adalpari Oct 27, 2025
0ef0205
detekt
adalpari Oct 27, 2025
836f3dd
TODO for debug purposes
adalpari Oct 27, 2025
e126063
Claude PR suggestions
adalpari Oct 27, 2025
ce8deba
Put ConversationListView in common between bots and HE
adalpari Oct 28, 2025
fa2f147
Empty and error state
adalpari Oct 28, 2025
971499a
Skip site capitalization
adalpari Oct 28, 2025
6ecf89d
Adding a11c labels
adalpari Oct 28, 2025
3edc398
Adding headings labels
adalpari Oct 28, 2025
ded3f34
adding accessible labels to chat bubbles
adalpari Oct 28, 2025
d8c4468
detekt
adalpari Oct 28, 2025
f97d4e8
Fixing tests
adalpari Oct 28, 2025
1294e3d
PR suggestion about bot chat bubble
adalpari Oct 28, 2025
f8dc40e
Fixing tests
adalpari Oct 28, 2025
983ea0d
Updating rust
adalpari Oct 28, 2025
471312e
Adding attachments UI
adalpari Oct 28, 2025
4744542
Parsing markdown more exhaustively
adalpari Oct 28, 2025
ce4641f
New links support
adalpari Oct 28, 2025
55d37a5
Detekt
adalpari Oct 28, 2025
8314472
Supporting in conversation as well
adalpari Oct 28, 2025
af50df5
Keeping the screen when select images
adalpari Oct 29, 2025
8223d9e
Add attachments to the message data class
adalpari Oct 29, 2025
2b75327
Showing attachments in the UI
adalpari Oct 29, 2025
49082a2
Downloading attachments
adalpari Oct 29, 2025
6e93ffd
detekt
adalpari Oct 29, 2025
1c6300d
Support pagination
adalpari Oct 27, 2025
53ec089
Triggering in the 4th element
adalpari Oct 27, 2025
7370761
detekt
adalpari Oct 27, 2025
7bd8919
TODO for debug purposes
adalpari Oct 27, 2025
9016a82
Claude PR suggestions
adalpari Oct 27, 2025
88fc5d2
Detekt
adalpari Oct 29, 2025
3d1ab98
Merge branch 'feat/CMM-883-support-Oddie-bot-conversation-pagination'…
adalpari Oct 29, 2025
d8affd9
Removing testing code
adalpari Oct 29, 2025
260ded1
Updating RS library version
adalpari Oct 30, 2025
109cc41
Opening images in fullscreen
adalpari Oct 30, 2025
0d782f0
Improving full screen image UX
adalpari Oct 30, 2025
e0d2955
Merge branch 'feat/CMM-883-support-Oddie-bot-conversation-pagination'…
adalpari Oct 30, 2025
f15af11
Improving semantics
adalpari Oct 30, 2025
c39e5ca
Merge branch 'trunk' into feat/CMM-885-support-HE-attachments
adalpari Oct 30, 2025
81d6d28
Extracting strings
adalpari Oct 30, 2025
5c31d48
Using rs PR fix
adalpari Oct 31, 2025
0f67996
Showing attachment preview
adalpari Oct 31, 2025
3343a03
Clearing attachments on new ticket screen close
adalpari Oct 31, 2025
f3e40c9
Removing selected images limit
adalpari Oct 31, 2025
ad1382a
Unifying attachments handling inside the VM
adalpari Oct 31, 2025
5dd1432
Using a launcher instead of startActivityForResult
adalpari Oct 31, 2025
72bad43
Remove unused parameter
adalpari Oct 31, 2025
f7795db
Handling temp files inside the VM
adalpari Oct 31, 2025
94bb5d8
Removing files
adalpari Oct 31, 2025
5a51b11
detekt
adalpari Oct 31, 2025
d1a9aaf
Throwing copy file error
adalpari Oct 31, 2025
310d9df
Extracting some individual composables from HEConversation screen file
adalpari Oct 31, 2025
09f138a
Reducing arguments
adalpari Oct 31, 2025
b0ebf82
Catch file creation error
adalpari Oct 31, 2025
848235f
Using proper file extension
adalpari Oct 31, 2025
9a502b2
General improvements
adalpari Oct 31, 2025
9f5ec65
Update RS version and some fixes
adalpari Oct 31, 2025
ce18ff0
Extracting temp attachment utils
adalpari Oct 31, 2025
9903bfb
Adding new tests
adalpari Oct 31, 2025
9ac2bd9
Some refactoring
adalpari Oct 31, 2025
c197b7c
Merge branch 'trunk' into feat/CMM-885-support-HE-attachments
adalpari Oct 31, 2025
9871cd8
Merge remote-tracking branch 'origin/trunk' into feat/CMM-885-support…
adalpari Oct 31, 2025
6daee20
Removing attachments preview to open a dedicated PR
adalpari Oct 31, 2025
3687944
Useless changes
adalpari Oct 31, 2025
529123f
Useless changes
adalpari Oct 31, 2025
602b014
Minor refactor
adalpari Nov 3, 2025
7890c15
Showing attachments previews
adalpari Nov 3, 2025
bbe0ae7
Typo
adalpari Nov 3, 2025
e65b8cc
String fix
adalpari Nov 3, 2025
eb02a33
Fixing pan issue
adalpari Nov 3, 2025
900cdaf
Passing attachments directly instead of searching for then when tappe…
adalpari Nov 3, 2025
2a4ac16
Merge branch 'feat/CMM-885-support-HE-attachments' into feat/CMM-885-…
adalpari Nov 3, 2025
315083a
Compile fix
adalpari Nov 3, 2025
031fb06
Fixing the send state message
adalpari Nov 3, 2025
1573520
Checking network availability
adalpari Nov 3, 2025
2006d63
Saving message state when error
adalpari Nov 3, 2025
80bedeb
Tests
adalpari Nov 3, 2025
0e058dc
Reverting non-related commits done by mistake
adalpari Nov 3, 2025
4867b58
Merge branch 'feat/CMM-885-support-HE-attachments-i2' into feat/CMM-9…
adalpari Nov 3, 2025
f0c2618
Reset files due to a wrong commit
adalpari Nov 3, 2025
0a3c577
Duplicated code and tests fix
adalpari Nov 3, 2025
c2e1a45
Merge branch 'trunk' into feat/CMM-926-support-offline-bug-and-better…
adalpari Nov 4, 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
Expand Up @@ -93,6 +93,7 @@ class AIBotSupportActivity : AppCompatActivity() {
val message = when (errorType) {
ConversationsSupportViewModel.ErrorType.GENERAL -> getString(R.string.ai_bot_generic_error)
ConversationsSupportViewModel.ErrorType.FORBIDDEN -> getString(R.string.he_support_forbidden_error)
ConversationsSupportViewModel.ErrorType.OFFLINE -> getString(R.string.no_network_title)
}
scope.launch {
snackbarHostState.showSnackbar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import org.wordpress.android.util.NetworkUtilsWrapper
abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
protected val accountStore: AccountStore,
protected val appLogWrapper: AppLogWrapper,
private val networkUtilsWrapper: NetworkUtilsWrapper,
protected val networkUtilsWrapper: NetworkUtilsWrapper,
) : ViewModel() {
sealed class NavigationEvent {
data object NavigateToConversationDetail : NavigationEvent()
Expand Down Expand Up @@ -140,6 +140,11 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
fun onConversationClick(conversation: ConversationType) {
viewModelScope.launch {
try {
if (!networkUtilsWrapper.isNetworkAvailable()) {
_errorMessage.value = ErrorType.OFFLINE
return@launch
}

_isLoadingConversation.value = true
_selectedConversation.value = conversation
_navigationEvents.emit(NavigationEvent.NavigateToConversationDetail)
Expand Down Expand Up @@ -173,6 +178,10 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(

fun onCreateNewConversationClick() {
viewModelScope.launch {
if (!networkUtilsWrapper.isNetworkAvailable()) {
_errorMessage.value = ErrorType.OFFLINE
return@launch
}
_navigationEvents.emit(NavigationEvent.NavigateToNewConversation)
}
}
Expand All @@ -182,5 +191,6 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
enum class ErrorType {
GENERAL,
FORBIDDEN,
OFFLINE,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,31 @@ fun HEConversationDetailScreen(
}

if (showBottomSheet) {
// Close the sheet when sending completes
LaunchedEffect(messageSendResult) {
if (messageSendResult != null) {
// Clear draft only on success
if (messageSendResult is HESupportViewModel.MessageSendResult.Success) {
draftMessageText = ""
draftIncludeAppLogs = false
}

// Dismiss sheet and clear result for both success and failure
onClearMessageSendResult()
scope.launch {
sheetState.hide()
}.invokeOnCompletion {
showBottomSheet = false
}
}
}

HEConversationReplyBottomSheet(
sheetState = sheetState,
isSending = isSendingMessage,
messageSendResult = messageSendResult,
initialMessageText = draftMessageText,
initialIncludeAppLogs = draftIncludeAppLogs,
onDismiss = { currentMessage, currentIncludeAppLogs ->
// Save draft message when closing without sending
draftMessageText = currentMessage
draftIncludeAppLogs = currentIncludeAppLogs
scope.launch {
Expand All @@ -186,14 +203,9 @@ fun HEConversationDetailScreen(
}
},
onSend = { message, includeAppLogs ->
draftMessageText = message
onSendMessage(message, includeAppLogs)
},
onMessageSentSuccessfully = {
// Clear draft after successful send
draftMessageText = ""
draftIncludeAppLogs = false
onClearMessageSendResult()
},
attachments = attachments,
attachmentActionsListener = attachmentActionsListener
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -37,38 +36,17 @@ import org.wordpress.android.support.he.util.AttachmentActionsListener
fun HEConversationReplyBottomSheet(
sheetState: androidx.compose.material3.SheetState,
isSending: Boolean = false,
messageSendResult: HESupportViewModel.MessageSendResult? = null,
initialMessageText: String = "",
initialIncludeAppLogs: Boolean = false,
onDismiss: (currentMessage: String, currentIncludeAppLogs: Boolean) -> Unit,
onSend: (String, Boolean) -> Unit,
onMessageSentSuccessfully: () -> Unit,
attachments: List<Uri> = emptyList(),
attachmentActionsListener: AttachmentActionsListener
) {
var messageText by remember { mutableStateOf(initialMessageText) }
var includeAppLogs by remember { mutableStateOf(initialIncludeAppLogs) }
val scrollState = rememberScrollState()

// Close the sheet when sending completes successfully
LaunchedEffect(messageSendResult) {
when (messageSendResult) {
is HESupportViewModel.MessageSendResult.Success -> {
// Message sent successfully, close the sheet and clear draft
onDismiss("", false)
onMessageSentSuccessfully()
}
is HESupportViewModel.MessageSendResult.Failure -> {
// Message failed to send, draft is saved onDismiss
// The error will be shown via snackbar from the Activity
onDismiss("", false)
}
null -> {
// No result yet, do nothing
}
}
}

ModalBottomSheet(
onDismissRequest = { onDismiss(messageText, includeAppLogs) },
sheetState = sheetState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class HESupportActivity : AppCompatActivity() {
val message = when (errorType) {
ConversationsSupportViewModel.ErrorType.GENERAL -> getString(R.string.he_support_generic_error)
ConversationsSupportViewModel.ErrorType.FORBIDDEN -> getString(R.string.he_support_forbidden_error)
ConversationsSupportViewModel.ErrorType.OFFLINE -> getString(R.string.no_network_title)
}
scope.launch {
snackbarHostState.showSnackbar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class HESupportViewModel @Inject constructor(
) {
viewModelScope.launch(ioDispatcher) {
try {
if (!networkUtilsWrapper.isNetworkAvailable()) {
_errorMessage.value = ErrorType.OFFLINE
return@launch
}

_isSendingMessage.value = true

val files = tempAttachmentsUtil.createTempFilesFrom(_attachments.value)
Expand Down Expand Up @@ -108,6 +113,12 @@ class HESupportViewModel @Inject constructor(
fun onAddMessageToConversation(message: String) {
viewModelScope.launch(ioDispatcher) {
try {
if (!networkUtilsWrapper.isNetworkAvailable()) {
_messageSendResult.value = MessageSendResult.Failure
_errorMessage.value = ErrorType.OFFLINE
return@launch
}

val selectedConversation = _selectedConversation.value
if (selectedConversation == null) {
appLogWrapper.e(AppLog.T.SUPPORT, "Error answering a conversation: no conversation selected")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
verify(appLogWrapper).e(any(), any<String>())
}

@Test
fun `init sets NoNetwork state when network is not available`() = test {
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)

viewModel.init()
advanceUntilIdle()

assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.NoNetwork.javaClass)
assertThat(viewModel.conversations.value).isEmpty()
}

// Refresh Conversations Tests

@Test
Expand Down Expand Up @@ -180,6 +191,22 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.Error.javaClass)
}

@Test
fun `refreshConversations sets NoNetwork state when network is not available`() = test {
val initialConversations = createTestConversations(count = 2)
viewModel.setConversationsToReturn(initialConversations)
viewModel.init()
advanceUntilIdle()

whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
viewModel.refreshConversations()
advanceUntilIdle()

assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.NoNetwork.javaClass)
// Conversations should remain unchanged from previous load
assertThat(viewModel.conversations.value).isEqualTo(initialConversations)
}

// Clear Error Tests

@Test
Expand Down Expand Up @@ -275,6 +302,37 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
verify(appLogWrapper).e(any(), any<String>())
}

@Test
fun `onConversationClick sets OFFLINE error when network is not available`() = test {
val conversation = createTestConversation(1)
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)

viewModel.onConversationClick(conversation)
advanceUntilIdle()

assertThat(viewModel.errorMessage.value).isEqualTo(ConversationsSupportViewModel.ErrorType.OFFLINE)
assertThat(viewModel.isLoadingConversation.value).isFalse
}

@Test
fun `onConversationClick does not navigate when network is not available`() = test {
val conversation = createTestConversation(1)
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)

var emittedEvent: ConversationsSupportViewModel.NavigationEvent? = null
val job = launch {
viewModel.navigationEvents.collect { event ->
emittedEvent = event
}
}

viewModel.onConversationClick(conversation)
advanceUntilIdle()

assertThat(emittedEvent).isNull()
job.cancel()
}

@Test
fun `onBackFromDetailClick clears selected conversation`() = test {
val conversation = createTestConversation(1)
Expand Down Expand Up @@ -322,6 +380,34 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
job.cancel()
}

@Test
fun `onCreateNewConversationClick sets OFFLINE error when network is not available`() = test {
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)

viewModel.onCreateNewConversationClick()
advanceUntilIdle()

assertThat(viewModel.errorMessage.value).isEqualTo(ConversationsSupportViewModel.ErrorType.OFFLINE)
}

@Test
fun `onCreateNewConversationClick does not navigate when network is not available`() = test {
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)

var emittedEvent: ConversationsSupportViewModel.NavigationEvent? = null
val job = launch {
viewModel.navigationEvents.collect { event ->
emittedEvent = event
}
}

viewModel.onCreateNewConversationClick()
advanceUntilIdle()

assertThat(emittedEvent).isNull()
job.cancel()
}

@Test
fun `setNewConversation sets selected conversation and emits navigation event`() = test {
val conversation = createTestConversation(1)
Expand Down
Loading