Skip to content

Commit 5db3cbf

Browse files
authored
CMM-926 support offline bug and better handling (#22334)
* Adding basic UI * Renaming * Some styling * Renaming and dummy data * Using proper "new conversation icon" * Conversation details screen * Creating the reply bottomsheet * Linking to the support screen * bottomsheet fix * Mov navigation form activity to viewmodel * Adding create ticket screen * More screen adjustments * Extracting common code * Margin fix * detekt * Style * New ticket check * Creating tests * Creating repository and load conversations function * Adding createConversation function * Creating loadConversation func * Loading conversations form the viewmodel * Adding loading spinner * Pull to refresh * Proper ionitialization * Adding empty screen * Handling send new conversation * Show loading when sending * New ticket creation fix * Using snackbar for errors * Error handling * Answering conversation * Adding some test to the repository * More tests! * Compile fixes * Similarities improvements * Using snackbar in bots activity * Extracting EmptyConversationsView * Renaming * Extracting VM and UI common code * Extracting navigation common code * Renaming VMs for clarification * More refactor * Capitalise text fields * Updating rs library * Loading conversation UX * Style fix * Fixing scaffolds paddings * userID fix * Fixing the padding problem in bot chat when the keyboard is opened * Apply padding to create ticket screen when the keyboard is opened * Fixing scroll state in reply bottomsheet * Adding tests for the new common viewmodel * Fixing AIBotSupportViewModel tests * detekt * Improvements int he conversation interaction * Adding tests for HE VM * Saving draft state * Properly navigating when a ticket is selected * Error parsing improvement * accessToken suggestion improvements * General suggestions * Send message error UX improvement * Fixing tests * Converting the UI to more AndroidMaterial style * Bots screen renaming * Bots screens renaming * Make NewTicket screen more Android Material theme as well * Adding preview for EmptyConversationsView * Button fix * detekt * Ticket selection change * Supporting markdown text * detekt * Improving MarkdownUtils * Formatting text in the repository layer instead the ui * Renaming * Fixing tests * Support pagination * Triggering in the 4th element * detekt * TODO for debug purposes * Claude PR suggestions Mutex and constant * 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 * Updating rust * Adding attachments UI * Parsing markdown more exhaustively * New links support * Detekt * Supporting in conversation as well * Keeping the screen when select images * Add attachments to the message data class * Showing attachments in the UI * Downloading attachments * detekt * Support pagination * Triggering in the 4th element * detekt * TODO for debug purposes * Claude PR suggestions Mutex and constant * Detekt * Removing testing code * Updating RS library version * Opening images in fullscreen * Improving full screen image UX * Improving semantics * Extracting strings * Using rs PR fix * Showing attachment preview * Clearing attachments on new ticket screen close * Removing selected images limit * Unifying attachments handling inside the VM * Using a launcher instead of startActivityForResult * Remove unused parameter * Handling temp files inside the VM * Removing files * detekt * Throwing copy file error * Extracting some individual composables from HEConversation screen file * Reducing arguments * Catch file creation error * Using proper file extension * General improvements * Update RS version and some fixes * Extracting temp attachment utils * Adding new tests * Some refactoring * Removing attachments preview to open a dedicated PR * Useless changes * Useless changes * Minor refactor * Showing attachments previews * Typo * String fix * Fixing pan issue * Passing attachments directly instead of searching for then when tapped for full screen * Compile fix * Fixing the send state message * Checking network availability * Saving message state when error * Tests * Reverting non-related commits done by mistake * Reset files due to a wrong commit * Duplicated code and tests fix
1 parent c52ebe2 commit 5db3cbf

File tree

8 files changed

+221
-31
lines changed

8 files changed

+221
-31
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class AIBotSupportActivity : AppCompatActivity() {
9393
val message = when (errorType) {
9494
ConversationsSupportViewModel.ErrorType.GENERAL -> getString(R.string.ai_bot_generic_error)
9595
ConversationsSupportViewModel.ErrorType.FORBIDDEN -> getString(R.string.he_support_forbidden_error)
96+
ConversationsSupportViewModel.ErrorType.OFFLINE -> getString(R.string.no_network_title)
9697
}
9798
scope.launch {
9899
snackbarHostState.showSnackbar(

WordPress/src/main/java/org/wordpress/android/support/common/ui/ConversationsSupportViewModel.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import org.wordpress.android.util.NetworkUtilsWrapper
1919
abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
2020
protected val accountStore: AccountStore,
2121
protected val appLogWrapper: AppLogWrapper,
22-
private val networkUtilsWrapper: NetworkUtilsWrapper,
22+
protected val networkUtilsWrapper: NetworkUtilsWrapper,
2323
) : ViewModel() {
2424
sealed class NavigationEvent {
2525
data object NavigateToConversationDetail : NavigationEvent()
@@ -140,6 +140,11 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
140140
fun onConversationClick(conversation: ConversationType) {
141141
viewModelScope.launch {
142142
try {
143+
if (!networkUtilsWrapper.isNetworkAvailable()) {
144+
_errorMessage.value = ErrorType.OFFLINE
145+
return@launch
146+
}
147+
143148
_isLoadingConversation.value = true
144149
_selectedConversation.value = conversation
145150
_navigationEvents.emit(NavigationEvent.NavigateToConversationDetail)
@@ -173,6 +178,10 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
173178

174179
fun onCreateNewConversationClick() {
175180
viewModelScope.launch {
181+
if (!networkUtilsWrapper.isNetworkAvailable()) {
182+
_errorMessage.value = ErrorType.OFFLINE
183+
return@launch
184+
}
176185
_navigationEvents.emit(NavigationEvent.NavigateToNewConversation)
177186
}
178187
}
@@ -182,5 +191,6 @@ abstract class ConversationsSupportViewModel<ConversationType: Conversation>(
182191
enum class ErrorType {
183192
GENERAL,
184193
FORBIDDEN,
194+
OFFLINE,
185195
}
186196
}

WordPress/src/main/java/org/wordpress/android/support/he/ui/HEConversationDetailScreen.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,31 @@ fun HEConversationDetailScreen(
169169
}
170170

171171
if (showBottomSheet) {
172+
// Close the sheet when sending completes
173+
LaunchedEffect(messageSendResult) {
174+
if (messageSendResult != null) {
175+
// Clear draft only on success
176+
if (messageSendResult is HESupportViewModel.MessageSendResult.Success) {
177+
draftMessageText = ""
178+
draftIncludeAppLogs = false
179+
}
180+
181+
// Dismiss sheet and clear result for both success and failure
182+
onClearMessageSendResult()
183+
scope.launch {
184+
sheetState.hide()
185+
}.invokeOnCompletion {
186+
showBottomSheet = false
187+
}
188+
}
189+
}
190+
172191
HEConversationReplyBottomSheet(
173192
sheetState = sheetState,
174193
isSending = isSendingMessage,
175-
messageSendResult = messageSendResult,
176194
initialMessageText = draftMessageText,
177195
initialIncludeAppLogs = draftIncludeAppLogs,
178196
onDismiss = { currentMessage, currentIncludeAppLogs ->
179-
// Save draft message when closing without sending
180197
draftMessageText = currentMessage
181198
draftIncludeAppLogs = currentIncludeAppLogs
182199
scope.launch {
@@ -186,14 +203,9 @@ fun HEConversationDetailScreen(
186203
}
187204
},
188205
onSend = { message, includeAppLogs ->
206+
draftMessageText = message
189207
onSendMessage(message, includeAppLogs)
190208
},
191-
onMessageSentSuccessfully = {
192-
// Clear draft after successful send
193-
draftMessageText = ""
194-
draftIncludeAppLogs = false
195-
onClearMessageSendResult()
196-
},
197209
attachments = attachments,
198210
attachmentActionsListener = attachmentActionsListener
199211
)

WordPress/src/main/java/org/wordpress/android/support/he/ui/HEConversationReplyBottomSheet.kt

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import androidx.compose.material3.ModalBottomSheet
1717
import androidx.compose.material3.Text
1818
import androidx.compose.material3.TextButton
1919
import androidx.compose.runtime.Composable
20-
import androidx.compose.runtime.LaunchedEffect
2120
import androidx.compose.runtime.getValue
2221
import androidx.compose.runtime.mutableStateOf
2322
import androidx.compose.runtime.remember
@@ -37,38 +36,17 @@ import org.wordpress.android.support.he.util.AttachmentActionsListener
3736
fun HEConversationReplyBottomSheet(
3837
sheetState: androidx.compose.material3.SheetState,
3938
isSending: Boolean = false,
40-
messageSendResult: HESupportViewModel.MessageSendResult? = null,
4139
initialMessageText: String = "",
4240
initialIncludeAppLogs: Boolean = false,
4341
onDismiss: (currentMessage: String, currentIncludeAppLogs: Boolean) -> Unit,
4442
onSend: (String, Boolean) -> Unit,
45-
onMessageSentSuccessfully: () -> Unit,
4643
attachments: List<Uri> = emptyList(),
4744
attachmentActionsListener: AttachmentActionsListener
4845
) {
4946
var messageText by remember { mutableStateOf(initialMessageText) }
5047
var includeAppLogs by remember { mutableStateOf(initialIncludeAppLogs) }
5148
val scrollState = rememberScrollState()
5249

53-
// Close the sheet when sending completes successfully
54-
LaunchedEffect(messageSendResult) {
55-
when (messageSendResult) {
56-
is HESupportViewModel.MessageSendResult.Success -> {
57-
// Message sent successfully, close the sheet and clear draft
58-
onDismiss("", false)
59-
onMessageSentSuccessfully()
60-
}
61-
is HESupportViewModel.MessageSendResult.Failure -> {
62-
// Message failed to send, draft is saved onDismiss
63-
// The error will be shown via snackbar from the Activity
64-
onDismiss("", false)
65-
}
66-
null -> {
67-
// No result yet, do nothing
68-
}
69-
}
70-
}
71-
7250
ModalBottomSheet(
7351
onDismissRequest = { onDismiss(messageText, includeAppLogs) },
7452
sheetState = sheetState

WordPress/src/main/java/org/wordpress/android/support/he/ui/HESupportActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class HESupportActivity : AppCompatActivity() {
123123
val message = when (errorType) {
124124
ConversationsSupportViewModel.ErrorType.GENERAL -> getString(R.string.he_support_generic_error)
125125
ConversationsSupportViewModel.ErrorType.FORBIDDEN -> getString(R.string.he_support_forbidden_error)
126+
ConversationsSupportViewModel.ErrorType.OFFLINE -> getString(R.string.no_network_title)
126127
}
127128
scope.launch {
128129
snackbarHostState.showSnackbar(

WordPress/src/main/java/org/wordpress/android/support/he/ui/HESupportViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class HESupportViewModel @Inject constructor(
5959
) {
6060
viewModelScope.launch(ioDispatcher) {
6161
try {
62+
if (!networkUtilsWrapper.isNetworkAvailable()) {
63+
_errorMessage.value = ErrorType.OFFLINE
64+
return@launch
65+
}
66+
6267
_isSendingMessage.value = true
6368

6469
val files = tempAttachmentsUtil.createTempFilesFrom(_attachments.value)
@@ -108,6 +113,12 @@ class HESupportViewModel @Inject constructor(
108113
fun onAddMessageToConversation(message: String) {
109114
viewModelScope.launch(ioDispatcher) {
110115
try {
116+
if (!networkUtilsWrapper.isNetworkAvailable()) {
117+
_messageSendResult.value = MessageSendResult.Failure
118+
_errorMessage.value = ErrorType.OFFLINE
119+
return@launch
120+
}
121+
111122
val selectedConversation = _selectedConversation.value
112123
if (selectedConversation == null) {
113124
appLogWrapper.e(AppLog.T.SUPPORT, "Error answering a conversation: no conversation selected")

WordPress/src/test/java/org/wordpress/android/support/common/ui/ConversationsSupportViewModelTest.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
148148
verify(appLogWrapper).e(any(), any<String>())
149149
}
150150

151+
@Test
152+
fun `init sets NoNetwork state when network is not available`() = test {
153+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
154+
155+
viewModel.init()
156+
advanceUntilIdle()
157+
158+
assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.NoNetwork.javaClass)
159+
assertThat(viewModel.conversations.value).isEmpty()
160+
}
161+
151162
// Refresh Conversations Tests
152163

153164
@Test
@@ -180,6 +191,22 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
180191
assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.Error.javaClass)
181192
}
182193

194+
@Test
195+
fun `refreshConversations sets NoNetwork state when network is not available`() = test {
196+
val initialConversations = createTestConversations(count = 2)
197+
viewModel.setConversationsToReturn(initialConversations)
198+
viewModel.init()
199+
advanceUntilIdle()
200+
201+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
202+
viewModel.refreshConversations()
203+
advanceUntilIdle()
204+
205+
assertThat(viewModel.conversationsState.value).isInstanceOf(ConversationsState.NoNetwork.javaClass)
206+
// Conversations should remain unchanged from previous load
207+
assertThat(viewModel.conversations.value).isEqualTo(initialConversations)
208+
}
209+
183210
// Clear Error Tests
184211

185212
@Test
@@ -275,6 +302,37 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
275302
verify(appLogWrapper).e(any(), any<String>())
276303
}
277304

305+
@Test
306+
fun `onConversationClick sets OFFLINE error when network is not available`() = test {
307+
val conversation = createTestConversation(1)
308+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
309+
310+
viewModel.onConversationClick(conversation)
311+
advanceUntilIdle()
312+
313+
assertThat(viewModel.errorMessage.value).isEqualTo(ConversationsSupportViewModel.ErrorType.OFFLINE)
314+
assertThat(viewModel.isLoadingConversation.value).isFalse
315+
}
316+
317+
@Test
318+
fun `onConversationClick does not navigate when network is not available`() = test {
319+
val conversation = createTestConversation(1)
320+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
321+
322+
var emittedEvent: ConversationsSupportViewModel.NavigationEvent? = null
323+
val job = launch {
324+
viewModel.navigationEvents.collect { event ->
325+
emittedEvent = event
326+
}
327+
}
328+
329+
viewModel.onConversationClick(conversation)
330+
advanceUntilIdle()
331+
332+
assertThat(emittedEvent).isNull()
333+
job.cancel()
334+
}
335+
278336
@Test
279337
fun `onBackFromDetailClick clears selected conversation`() = test {
280338
val conversation = createTestConversation(1)
@@ -322,6 +380,34 @@ class ConversationsSupportViewModelTest : BaseUnitTest() {
322380
job.cancel()
323381
}
324382

383+
@Test
384+
fun `onCreateNewConversationClick sets OFFLINE error when network is not available`() = test {
385+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
386+
387+
viewModel.onCreateNewConversationClick()
388+
advanceUntilIdle()
389+
390+
assertThat(viewModel.errorMessage.value).isEqualTo(ConversationsSupportViewModel.ErrorType.OFFLINE)
391+
}
392+
393+
@Test
394+
fun `onCreateNewConversationClick does not navigate when network is not available`() = test {
395+
whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
396+
397+
var emittedEvent: ConversationsSupportViewModel.NavigationEvent? = null
398+
val job = launch {
399+
viewModel.navigationEvents.collect { event ->
400+
emittedEvent = event
401+
}
402+
}
403+
404+
viewModel.onCreateNewConversationClick()
405+
advanceUntilIdle()
406+
407+
assertThat(emittedEvent).isNull()
408+
job.cancel()
409+
}
410+
325411
@Test
326412
fun `setNewConversation sets selected conversation and emits navigation event`() = test {
327413
val conversation = createTestConversation(1)

0 commit comments

Comments
 (0)