Initial commit
This commit is contained in:
commit
6072b2ba29
844 changed files with 220532 additions and 0 deletions
274
app/src/main/java/com/aryan/reader/feedback/FeedbackViewModel.kt
Normal file
274
app/src/main/java/com/aryan/reader/feedback/FeedbackViewModel.kt
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
package com.aryan.reader.feedback
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import timber.log.Timber
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aryan.reader.AuthRepository
|
||||
import com.aryan.reader.data.FeedbackMessage
|
||||
import com.aryan.reader.data.FeedbackRepository
|
||||
import com.aryan.reader.data.FeedbackThread
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
|
||||
data class FeedbackUiState(
|
||||
val threads: List<FeedbackThread> = emptyList(),
|
||||
val currentMessages: List<FeedbackMessage> = emptyList(),
|
||||
val pendingMessages: List<FeedbackMessage> = emptyList(),
|
||||
val selectedThreadId: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val isCreatingTicket: Boolean = false,
|
||||
val newTicketMessage: String = "",
|
||||
val newTicketCategory: String = "Bug Report",
|
||||
val newTicketAttachments: List<Uri> = emptyList(),
|
||||
val chatInputMessage: String = "",
|
||||
val chatInputAttachments: List<Uri> = emptyList()
|
||||
)
|
||||
|
||||
class FeedbackViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository = FeedbackRepository(application)
|
||||
private val authRepository = AuthRepository(application)
|
||||
private val _uiState = MutableStateFlow(FeedbackUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private var threadsListener: Any? = null
|
||||
private var messagesListener: Any? = null
|
||||
private val currentUser = authRepository.getSignedInUser()
|
||||
|
||||
init {
|
||||
if (currentUser != null) {
|
||||
startListeningToThreads(currentUser.uid)
|
||||
} else {
|
||||
_uiState.update { it.copy(errorMessage = "You must be signed in to use feedback.") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun startListeningToThreads(userId: String) {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
threadsListener = repository.listenToFeedbackThreads(userId) { threads ->
|
||||
_uiState.update { it.copy(threads = threads, isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onThreadSelected(threadId: String) {
|
||||
Timber.d("VM: onThreadSelected $threadId")
|
||||
repository.removeListener(messagesListener)
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
selectedThreadId = threadId,
|
||||
isLoading = true,
|
||||
currentMessages = emptyList(),
|
||||
pendingMessages = emptyList(),
|
||||
chatInputMessage = "",
|
||||
chatInputAttachments = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
repository.markThreadAsRead(threadId)
|
||||
}
|
||||
|
||||
messagesListener = repository.listenToMessages(threadId) { messages ->
|
||||
Timber.d("VM: Listener update received. ${messages.size} messages.")
|
||||
viewModelScope.launch {
|
||||
repository.markThreadAsRead(threadId)
|
||||
}
|
||||
|
||||
_uiState.update { state ->
|
||||
val realIds = messages.map { it.id }.toSet()
|
||||
val remainingPending = state.pendingMessages.filter { it.id !in realIds }
|
||||
|
||||
if (remainingPending.size != state.pendingMessages.size) {
|
||||
Timber.d("VM: Reconciliation - Removed ${state.pendingMessages.size - remainingPending.size} pending messages.")
|
||||
}
|
||||
|
||||
state.copy(
|
||||
currentMessages = messages,
|
||||
pendingMessages = remainingPending,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackToThreadList() {
|
||||
Timber.d("VM: Back to thread list")
|
||||
repository.removeListener(messagesListener)
|
||||
messagesListener = null
|
||||
_uiState.update { it.copy(selectedThreadId = null, currentMessages = emptyList(), pendingMessages = emptyList()) }
|
||||
}
|
||||
|
||||
fun onStartCreateTicket() {
|
||||
if (currentUser == null) {
|
||||
_uiState.update { it.copy(errorMessage = "You must be signed in to submit feedback.") }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(isCreatingTicket = true) }
|
||||
}
|
||||
|
||||
fun onCancelCreateTicket() {
|
||||
_uiState.update { it.copy(isCreatingTicket = false, newTicketMessage = "", newTicketAttachments = emptyList()) }
|
||||
}
|
||||
|
||||
fun onNewTicketMessageChange(text: String) {
|
||||
_uiState.update { it.copy(newTicketMessage = text) }
|
||||
}
|
||||
|
||||
fun onNewTicketCategoryChange(category: String) {
|
||||
_uiState.update { it.copy(newTicketCategory = category) }
|
||||
}
|
||||
|
||||
fun onNewTicketImagesSelected(uris: List<Uri>) {
|
||||
val current = _uiState.value.newTicketAttachments
|
||||
if (current.size + uris.size > 3) {
|
||||
_uiState.update { it.copy(errorMessage = "Max 3 images allowed for tickets.") }
|
||||
return
|
||||
}
|
||||
validateAndAddImages(uris) { validUris ->
|
||||
_uiState.update { it.copy(newTicketAttachments = it.newTicketAttachments + validUris) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onRemoveNewTicketImage(uri: Uri) {
|
||||
_uiState.update { it.copy(newTicketAttachments = it.newTicketAttachments - uri) }
|
||||
}
|
||||
|
||||
fun onChatInputChange(text: String) {
|
||||
_uiState.update { it.copy(chatInputMessage = text) }
|
||||
}
|
||||
|
||||
fun onChatImagesSelected(uris: List<Uri>) {
|
||||
val current = _uiState.value.chatInputAttachments
|
||||
if (current.size + uris.size > 5) {
|
||||
_uiState.update { it.copy(errorMessage = "Max 5 images allowed per message.") }
|
||||
return
|
||||
}
|
||||
validateAndAddImages(uris) { validUris ->
|
||||
_uiState.update { it.copy(chatInputAttachments = it.chatInputAttachments + validUris) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onRemoveChatImage(uri: Uri) {
|
||||
_uiState.update { it.copy(chatInputAttachments = it.chatInputAttachments - uri) }
|
||||
}
|
||||
|
||||
private fun validateAndAddImages(uris: List<Uri>, onValid: (List<Uri>) -> Unit) {
|
||||
val context = getApplication<Application>()
|
||||
val maxFileSize = 5 * 1024 * 1024 // 5 MB
|
||||
val validUris = mutableListOf<Uri>()
|
||||
for (uri in uris) {
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize > maxFileSize) {
|
||||
_uiState.update { it.copy(errorMessage = "One or more images exceed the 5MB limit.") }
|
||||
return
|
||||
}
|
||||
validUris.add(uri)
|
||||
}
|
||||
onValid(validUris)
|
||||
}
|
||||
|
||||
private fun getFileSize(context: Context, uri: Uri): Long {
|
||||
return try {
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.use { it.statSize } ?: 0L
|
||||
} catch (_: Exception) { 0L }
|
||||
}
|
||||
|
||||
fun onSubmitTicket() {
|
||||
val state = _uiState.value
|
||||
if (state.newTicketMessage.isBlank()) return
|
||||
val user = currentUser ?: return
|
||||
|
||||
onCancelCreateTicket()
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
try {
|
||||
val threadId = repository.createThread(
|
||||
userId = user.uid,
|
||||
category = state.newTicketCategory,
|
||||
message = state.newTicketMessage,
|
||||
attachmentUris = state.newTicketAttachments
|
||||
)
|
||||
onThreadSelected(threadId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "ViewModel: Error submitting ticket")
|
||||
_uiState.update { it.copy(errorMessage = "Failed to create ticket: ${e.message}") }
|
||||
} finally {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onSendMessage() {
|
||||
val state = _uiState.value
|
||||
val threadId = state.selectedThreadId ?: return
|
||||
val textToSend = state.chatInputMessage.trim()
|
||||
val attachmentsToSend = state.chatInputAttachments
|
||||
|
||||
if (textToSend.isBlank() && attachmentsToSend.isEmpty()) return
|
||||
val user = currentUser ?: return
|
||||
|
||||
val messageId = repository.generateMessageId()
|
||||
Timber.d("VM: Sending message. Generated ID: $messageId")
|
||||
|
||||
// Updated reference to id
|
||||
val pendingMessage = FeedbackMessage(
|
||||
id = messageId,
|
||||
text = textToSend,
|
||||
sender = "user",
|
||||
timestamp = Date(),
|
||||
attachments = attachmentsToSend.map { it.toString() }
|
||||
)
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
chatInputMessage = "",
|
||||
chatInputAttachments = emptyList(),
|
||||
pendingMessages = it.pendingMessages + pendingMessage
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.addMessage(
|
||||
threadId = threadId,
|
||||
messageId = messageId,
|
||||
uid = user.uid,
|
||||
message = textToSend,
|
||||
sender = "user",
|
||||
attachmentUris = attachmentsToSend
|
||||
)
|
||||
Timber.d("VM: addMessage returned success for $messageId")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "ViewModel: Error sending message")
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
errorMessage = "Failed to send: ${e.message}",
|
||||
// Updated reference to id
|
||||
pendingMessages = it.pendingMessages.filterNot { msg -> msg.id == messageId },
|
||||
chatInputMessage = textToSend
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.update { it.copy(errorMessage = null) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.removeListener(messagesListener)
|
||||
repository.removeListener(threadsListener)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue