Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
177 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
1bc69bc
Checking attachments size
adalpari Nov 4, 2025
35cad9c
Merge branch 'trunk' into feat/CMM-927-support-limit-attachments
adalpari Nov 5, 2025
4d0a649
Showing skipped files more intuitively
adalpari Nov 5, 2025
26fd5d0
Detekt
adalpari Nov 5, 2025
26aca42
Fixing and adding tests
adalpari Nov 5, 2025
0173798
detekt
adalpari Nov 5, 2025
7e6a628
PR suggestions
adalpari Nov 5, 2025
374d215
Fixing the incongruence between tests and rejection reason priority
adalpari Nov 5, 2025
1a67f93
Some refactor
adalpari Nov 5, 2025
f101e4f
Using a progressbar and retry adding skipped files
adalpari Nov 6, 2025
50be686
Merge branch 'trunk' into feat/CMM-927-support-limit-attachments
adalpari Nov 6, 2025
a53fe27
Fixing tests
adalpari Nov 6, 2025
cba99f9
Merge branch 'trunk' into feat/CMM-927-support-limit-attachments
adalpari Nov 10, 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,10 @@
package org.wordpress.android.support.he.model

import android.net.Uri

data class AttachmentState(
val acceptedUris: List<Uri> = emptyList(),
val rejectedUris: List<Uri> = emptyList(),
val currentTotalSizeBytes: Long = 0L,
val rejectedTotalSizeBytes: Long = 0L
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.wordpress.android.support.he.model

sealed class MessageSendResult {
data object Success : MessageSendResult()
data object Failure : MessageSendResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import coil.request.ImageRequest
import coil.request.videoFrameMillis
import org.wordpress.android.R
import org.wordpress.android.support.aibot.util.formatRelativeTime
import org.wordpress.android.support.he.model.AttachmentState
import org.wordpress.android.support.he.model.MessageSendResult
import org.wordpress.android.support.he.model.AttachmentType
import org.wordpress.android.support.he.model.SupportAttachment
import org.wordpress.android.support.he.model.SupportConversation
Expand All @@ -77,11 +79,11 @@ fun HEConversationDetailScreen(
conversation: SupportConversation,
isLoading: Boolean = false,
isSendingMessage: Boolean = false,
messageSendResult: HESupportViewModel.MessageSendResult? = null,
messageSendResult: MessageSendResult? = null,
onBackClick: () -> Unit,
onSendMessage: (message: String, includeAppLogs: Boolean) -> Unit,
onClearMessageSendResult: () -> Unit = {},
attachments: List<Uri> = emptyList(),
attachmentState: AttachmentState = AttachmentState(),
attachmentActionsListener: AttachmentActionsListener,
onDownloadAttachment: (SupportAttachment) -> Unit = {},
videoUrlResolver: org.wordpress.android.support.he.util.VideoUrlResolver? = null
Expand Down Expand Up @@ -178,7 +180,7 @@ fun HEConversationDetailScreen(
LaunchedEffect(messageSendResult) {
if (messageSendResult != null) {
// Clear draft only on success
if (messageSendResult is HESupportViewModel.MessageSendResult.Success) {
if (messageSendResult is MessageSendResult.Success) {
draftMessageText = ""
draftIncludeAppLogs = false
}
Expand Down Expand Up @@ -211,7 +213,13 @@ fun HEConversationDetailScreen(
draftMessageText = message
onSendMessage(message, includeAppLogs)
},
attachments = attachments,
onMessageSentSuccessfully = {
// Clear draft after successful send
draftMessageText = ""
draftIncludeAppLogs = false
onClearMessageSendResult()
},
attachmentState = attachmentState,
attachmentActionsListener = attachmentActionsListener
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.wordpress.android.support.he.ui

import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -17,6 +16,7 @@ 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 @@ -29,24 +29,47 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
import org.wordpress.android.support.he.model.AttachmentState
import org.wordpress.android.support.he.model.MessageSendResult
import org.wordpress.android.support.he.util.AttachmentActionsListener

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HEConversationReplyBottomSheet(
sheetState: androidx.compose.material3.SheetState,
isSending: Boolean = false,
messageSendResult: MessageSendResult? = null,
initialMessageText: String = "",
initialIncludeAppLogs: Boolean = false,
onDismiss: (currentMessage: String, currentIncludeAppLogs: Boolean) -> Unit,
onSend: (String, Boolean) -> Unit,
attachments: List<Uri> = emptyList(),
onMessageSentSuccessfully: () -> Unit,
attachmentState: AttachmentState = AttachmentState(),
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 MessageSendResult.Success -> {
// Message sent successfully, close the sheet and clear draft
onDismiss("", false)
onMessageSentSuccessfully()
}
is 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 Expand Up @@ -107,7 +130,7 @@ fun HEConversationReplyBottomSheet(
onMessageChanged = { message -> messageText = message },
onIncludeAppLogsChanged = { checked -> includeAppLogs = checked },
enabled = !isSending,
attachments = attachments,
attachmentState = attachmentState,
attachmentActionsListener = attachmentActionsListener
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import org.wordpress.android.R
import org.wordpress.android.support.common.model.UserInfo
import org.wordpress.android.support.he.model.AttachmentState
import org.wordpress.android.support.he.util.AttachmentActionsListener
import org.wordpress.android.ui.compose.components.MainTopAppBar
import org.wordpress.android.ui.compose.components.NavigationIcons
Expand All @@ -74,8 +75,8 @@ fun HENewTicketScreen(
) -> Unit,
userInfo: UserInfo,
isSendingNewConversation: Boolean = false,
attachments: List<Uri> = emptyList(),
attachmentActionsListener: AttachmentActionsListener,
attachmentState: AttachmentState = AttachmentState(),
attachmentActionsListener: AttachmentActionsListener
) {
var selectedCategory by remember { mutableStateOf<SupportCategory?>(null) }
var subject by remember { mutableStateOf("") }
Expand Down Expand Up @@ -197,7 +198,7 @@ fun HENewTicketScreen(
includeAppLogs = includeAppLogs,
onMessageChanged = { message -> messageText = message },
onIncludeAppLogsChanged = { checked -> includeAppLogs = checked },
attachments = attachments,
attachmentState = attachmentState,
attachmentActionsListener = attachmentActionsListener
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class HESupportActivity : AppCompatActivity() {
val isLoadingConversation by viewModel.isLoadingConversation.collectAsState()
val isSendingMessage by viewModel.isSendingMessage.collectAsState()
val messageSendResult by viewModel.messageSendResult.collectAsState()
val attachments by viewModel.attachments.collectAsState()
val attachmentState by viewModel.attachmentState.collectAsState()

selectedConversation?.let { conversation ->
HEConversationDetailScreen(
Expand All @@ -190,7 +190,7 @@ class HESupportActivity : AppCompatActivity() {
)
},
onClearMessageSendResult = { viewModel.clearMessageSendResult() },
attachments = attachments,
attachmentState = attachmentState,
attachmentActionsListener = createAttachmentActionListener(),
onDownloadAttachment = { attachment ->
// Show loading snackbar
Expand All @@ -213,7 +213,7 @@ class HESupportActivity : AppCompatActivity() {
composable(route = ConversationScreen.NewTicket.name) {
val userInfo by viewModel.userInfo.collectAsState()
val isSendingNewConversation by viewModel.isSendingMessage.collectAsState()
val attachments by viewModel.attachments.collectAsState()
val attachmentState by viewModel.attachmentState.collectAsState()

// Clear attachments when leaving the new ticket screen
androidx.compose.runtime.DisposableEffect(Unit) {
Expand All @@ -234,7 +234,7 @@ class HESupportActivity : AppCompatActivity() {
},
userInfo = userInfo,
isSendingNewConversation = isSendingNewConversation,
attachments = attachments,
attachmentState = attachmentState,
attachmentActionsListener = createAttachmentActionListener()
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.wordpress.android.support.he.ui

import android.app.Application
import android.net.Uri
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -8,10 +9,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.modules.IO_THREAD
import org.wordpress.android.support.common.ui.ConversationsSupportViewModel
import org.wordpress.android.support.he.model.AttachmentState
import org.wordpress.android.support.he.model.MessageSendResult
import org.wordpress.android.support.he.model.SupportConversation
import org.wordpress.android.support.he.repository.CreateConversationResult
import org.wordpress.android.support.he.repository.HESupportRepository
Expand All @@ -26,24 +30,23 @@ class HESupportViewModel @Inject constructor(
private val heSupportRepository: HESupportRepository,
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
private val tempAttachmentsUtil: TempAttachmentsUtil,
private val application: Application,
accountStore: AccountStore,
appLogWrapper: AppLogWrapper,
networkUtilsWrapper: NetworkUtilsWrapper,
) : ConversationsSupportViewModel<SupportConversation>(accountStore, appLogWrapper, networkUtilsWrapper) {
companion object {
const val MAX_TOTAL_SIZE_BYTES = 20L * 1024 * 1024 // 20MB total
}
private val _isSendingMessage = MutableStateFlow(false)
val isSendingMessage: StateFlow<Boolean> = _isSendingMessage.asStateFlow()

private val _messageSendResult = MutableStateFlow<MessageSendResult?>(null)
val messageSendResult: StateFlow<MessageSendResult?> = _messageSendResult.asStateFlow()

// Attachment state (shared for both Detail and NewTicket screens)
private val _attachments = MutableStateFlow<List<Uri>>(emptyList())
val attachments: StateFlow<List<Uri>> = _attachments.asStateFlow()

sealed class MessageSendResult {
data object Success : MessageSendResult()
data object Failure : MessageSendResult()
}
// Unified attachment state (shared for both Detail and NewTicket screens)
private val _attachmentState = MutableStateFlow(AttachmentState())
val attachmentState: StateFlow<AttachmentState> = _attachmentState.asStateFlow()

override fun initRepository(accessToken: String) {
heSupportRepository.init(accessToken)
Expand All @@ -66,7 +69,7 @@ class HESupportViewModel @Inject constructor(

_isSendingMessage.value = true

val files = tempAttachmentsUtil.createTempFilesFrom(_attachments.value)
val files = tempAttachmentsUtil.createTempFilesFrom(_attachmentState.value.acceptedUris)

when (val result = heSupportRepository.createConversation(
subject = subject,
Expand All @@ -79,7 +82,7 @@ class HESupportViewModel @Inject constructor(
// update conversations locally
_conversations.value = listOf(newConversation) + _conversations.value
// Clear attachments after successful creation
_attachments.value = emptyList()
_attachmentState.value = AttachmentState()
onBackClick()
}

Expand Down Expand Up @@ -126,7 +129,7 @@ class HESupportViewModel @Inject constructor(
}

_isSendingMessage.value = true
val files = tempAttachmentsUtil.createTempFilesFrom(_attachments.value)
val files = tempAttachmentsUtil.createTempFilesFrom(_attachmentState.value.acceptedUris)

when (val result = heSupportRepository.addMessageToConversation(
conversationId = selectedConversation.id,
Expand All @@ -137,7 +140,7 @@ class HESupportViewModel @Inject constructor(
_selectedConversation.value = result.conversation
_messageSendResult.value = MessageSendResult.Success
// Clear attachments after successful message send
_attachments.value = emptyList()
_attachmentState.value = AttachmentState()
}

is CreateConversationResult.Error.Forbidden -> {
Expand Down Expand Up @@ -170,15 +173,103 @@ class HESupportViewModel @Inject constructor(
}

fun addAttachments(uris: List<Uri>) {
_attachments.value = _attachments.value + uris
viewModelScope.launch(ioDispatcher) {
_attachmentState.value = validateAndCreateAttachmentState(uris)
}
}

@Suppress("LoopWithTooManyJumpStatements")
private suspend fun validateAndCreateAttachmentState(uris: List<Uri>): AttachmentState = withContext(ioDispatcher) {
if (uris.isEmpty()) {
return@withContext _attachmentState.value
}

val validUris = mutableListOf<Uri>()
val skippedUris = mutableListOf<Uri>()

// Calculate current total size
var currentTotalSize = calculateTotalSize(_attachmentState.value.acceptedUris)

// Validate each new attachment
for (uri in uris) {
val fileSize = getFileSize(uri)

// Skip if we can't determine file size we just allow it to be added
if (fileSize != null) {
// Check if adding this file would exceed total size limit
if (currentTotalSize + fileSize > MAX_TOTAL_SIZE_BYTES) {
skippedUris.add(uri)
continue
}
}

// File is valid, add it
validUris.add(uri)
currentTotalSize += fileSize ?: 0
}

// Build the new attachment state
val currentAccepted = _attachmentState.value.acceptedUris
val newAccepted = currentAccepted + validUris

// Calculate rejected total size
val rejectedTotalSize = calculateTotalSize(skippedUris)

AttachmentState(
acceptedUris = newAccepted,
rejectedUris = skippedUris,
currentTotalSizeBytes = currentTotalSize,
rejectedTotalSizeBytes = rejectedTotalSize
)
}

@Suppress("TooGenericExceptionCaught")
private suspend fun getFileSize(uri: Uri): Long? = withContext(ioDispatcher) {
try {
application.contentResolver.openAssetFileDescriptor(uri, "r")?.use { descriptor ->
descriptor.length
}
} catch (e: Exception) {
appLogWrapper.d(AppLog.T.SUPPORT, "Could not determine file size for URI: $uri - ${e.message}")
// Silently return null if we can't get the file size
// This will be handled by the validation logic
null
}
}

/**
* Calculates the total size of all files in the list
* @param uris List of URIs to calculate size for
* @return Total size in bytes
*/
private suspend fun calculateTotalSize(uris: List<Uri>): Long {
var totalSize = 0L
for (uri in uris) {
totalSize += getFileSize(uri) ?: 0L
}
return totalSize
}

/**
* Removes an attachment from the accepted list and attempts to re-include any previously
* skipped files that can now fit within the size limit.
*
* This function removes the specified URI and then re-validates all previously skipped files
* by calling [addAttachments], which ensures consistent validation logic and automatically
* includes files that now fit within the available space.
*/
fun removeAttachment(uri: Uri) {
_attachments.value = _attachments.value.filter { it != uri }
viewModelScope.launch {
// Remove the attachment and re-validate skipped files
val currentState = _attachmentState.value.copy()
val newAcceptedUris = currentState.acceptedUris.filter { it != uri }
_attachmentState.value = currentState.copy(acceptedUris = newAcceptedUris)
addAttachments(currentState.rejectedUris)
}
}

fun clearAttachments() {
_attachments.value = emptyList()
_attachmentState.value = AttachmentState()
}

fun notifyGeneralError() {
Expand Down
Loading