From 222af69a7155664b8cf0b7219984be380ef15c3b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:03:59 +0400 Subject: [PATCH] multiplatform: file errors (#4289) --- .../chat/simplex/common/model/ChatModel.kt | 27 ++- .../chat/simplex/common/model/SimpleXAPI.kt | 20 ++- .../common/views/chat/ChatItemInfoView.kt | 30 ++++ .../common/views/chat/item/CIFileView.kt | 22 +++ .../common/views/chat/item/CIImageView.kt | 36 +++- .../common/views/chat/item/CIVIdeoView.kt | 58 +++++- .../common/views/chat/item/CIVoiceView.kt | 167 +++++++++++++++--- .../commonMain/resources/MR/base/strings.xml | 11 ++ 8 files changed, 324 insertions(+), 47 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 74a54e54cf..89af46edb5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2660,6 +2660,7 @@ data class CIFile( is CIFileStatus.SndComplete -> true is CIFileStatus.SndCancelled -> true is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false @@ -2667,6 +2668,7 @@ data class CIFile( is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false + is CIFileStatus.RcvWarning -> false is CIFileStatus.Invalid -> false } @@ -2682,6 +2684,7 @@ data class CIFile( } is CIFileStatus.SndCancelled -> null is CIFileStatus.SndError -> null + is CIFileStatus.SndWarning -> sndCancelAction is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction @@ -2689,6 +2692,7 @@ data class CIFile( is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null + is CIFileStatus.RcvWarning -> rcvCancelAction is CIFileStatus.Invalid -> null } @@ -2862,14 +2866,16 @@ sealed class CIFileStatus { @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus() @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus() @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus() - @Serializable @SerialName("sndError") object SndError: CIFileStatus() + @Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() - @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() + @Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus() + @Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus() val sent: Boolean get() = when (this) { @@ -2878,6 +2884,7 @@ sealed class CIFileStatus { is SndComplete -> true is SndCancelled -> true is SndError -> true + is SndWarning -> true is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false @@ -2885,10 +2892,26 @@ sealed class CIFileStatus { is RcvComplete -> false is RcvCancelled -> false is RcvError -> false + is RcvWarning -> false is Invalid -> false } } +@Serializable +sealed class FileError { + @Serializable @SerialName("auth") class Auth: FileError() + @Serializable @SerialName("noFile") class NoFile: FileError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError() + @Serializable @SerialName("other") class Other(val fileError: String): FileError() + + val errorInfo: String get() = when (this) { + is FileError.Auth -> generalGetString(MR.strings.file_error_auth) + is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file) + is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo) + is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError) + } +} + @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = MsgContentSerializer::class) sealed class MsgContent { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index becacc3932..839792e61c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2026,6 +2026,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.RcvFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.SndFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.SndFileComplete -> { @@ -2052,6 +2057,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.SndFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.CallInvitation -> { chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId)) } @@ -4322,6 +4332,7 @@ sealed class CR { @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileWarning") class RcvFileWarning(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() // sending file events @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @@ -4334,7 +4345,8 @@ sealed class CR { @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List): CR() @Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() - @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() + @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() + @Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() // call events @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List): CR() @@ -4493,6 +4505,7 @@ sealed class CR { is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" is RcvFileError -> "rcvFileError" + is RcvFileWarning -> "rcvFileWarning" is SndFileStart -> "sndFileStart" is SndFileComplete -> "sndFileComplete" is SndFileRcvCancelled -> "sndFileRcvCancelled" @@ -4502,6 +4515,7 @@ sealed class CR { is SndStandaloneFileComplete -> "sndStandaloneFileComplete" is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" + is SndFileWarning -> "sndFileWarning" is CallInvitations -> "callInvitations" is CallInvitation -> "callInvitation" is CallOffer -> "callOffer" @@ -4652,6 +4666,7 @@ sealed class CR { is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") is RcvStandaloneFileComplete -> withUser(user, targetPath) is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") + is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") is SndFileCancelled -> json.encodeToString(chatItem_) is SndStandaloneFileCreated -> noDetails() is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) @@ -4663,7 +4678,8 @@ sealed class CR { is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString()) is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_)) - is SndFileError -> withUser(user, json.encodeToString(chatItem_)) + is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") + is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}" is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 1e564db134..c23a6ad00e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -31,6 +31,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import kotlinx.serialization.encodeToString sealed class CIInfoTab { class Delivery(val memberDeliveryStatuses: List): CIInfoTab() @@ -216,6 +217,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } + @Composable + fun ExpandableInfoRow(title: String, value: String) { + val expanded = remember { mutableStateOf(false) } + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = 46.dp) + .padding(PaddingValues(horizontal = DEFAULT_PADDING)) + .clickable { expanded.value = !expanded.value }, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, color = MaterialTheme.colors.onBackground) + if (expanded.value) { + Text(value, color = MaterialTheme.colors.secondary) + } else { + Text(value, color = MaterialTheme.colors.secondary, maxLines = 1) + } + } + } + @Composable fun Details() { AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) @@ -244,6 +266,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (devTools) { InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString()) InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) + ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus)) + if (ci.file != null) { + ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus)) + } } } } @@ -531,6 +557,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem if (devTools) { shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) + shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus))) + if (ci.file != null) { + shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus))) + } } val qi = ci.quotedItem if (qi != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 0ce9ba32fc..89751dd140 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -87,6 +87,26 @@ fun CIFileView( ) FileProtocol.LOCAL -> {} } + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) file.forwardingAllowed() -> { withLongRunningApi(slow = 600_000) { var filePath = getLoadedFilePath(file) @@ -157,6 +177,7 @@ fun CIFileView( is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.RcvInvitation -> if (fileSizeValid(file)) fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) @@ -174,6 +195,7 @@ fun CIFileView( is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark)) } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 7cffe4564b..5aa3bfab05 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -70,14 +70,16 @@ fun CIImageView( is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvComplete -> {} is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } @@ -201,8 +203,8 @@ fun CIImageView( } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { - when (file.fileStatus) { - CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> + when { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> if (fileSizeValid()) { receiveFile(file.fileId) } else { @@ -211,7 +213,7 @@ fun CIImageView( String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) ) } - CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -225,9 +227,29 @@ fun CIImageView( ) FileProtocol.LOCAL -> {} } - CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? - CIFileStatus.RcvComplete -> {} // ? - CIFileStatus.RcvCancelled -> {} // TODO + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvTransfer -> {} // ? + file.fileStatus is CIFileStatus.RcvComplete -> {} // ? + file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO else -> {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index 749816f918..e655b73b02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -107,7 +107,7 @@ fun CIVideoView( } } } - loadingIndicator(file) + fileStatusIcon(file) } } @@ -339,11 +339,13 @@ private fun progressIndicator() { } @Composable -private fun fileIcon(icon: Painter, stringId: StringResource) { +private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) { + var modifier = Modifier.fillMaxSize() + modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier } Icon( icon, stringResource(stringId), - Modifier.fillMaxSize(), + modifier, tint = Color.White ) } @@ -364,7 +366,7 @@ private fun progressCircle(progress: Long, total: Long) { } @Composable -private fun loadingIndicator(file: CIFile?) { +private fun fileStatusIcon(file: CIFile?) { if (file != null) { Box( Modifier @@ -387,7 +389,28 @@ private fun loadingIndicator(file: CIFile?) { } is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + is CIFileStatus.SndWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) is CIFileStatus.RcvTransfer -> @@ -397,10 +420,31 @@ private fun loadingIndicator(file: CIFile?) { progressIndicator() } is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) + is CIFileStatus.RcvComplete -> {} is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + is CIFileStatus.RcvWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 96aaf586e9..040dd97474 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -252,6 +252,81 @@ private fun PlayPauseButton( } } +@Composable +private fun PlayablePlayPauseButton( + audioPlaying: Boolean, + sent: Boolean, + hasText: Boolean, + progress: State, + duration: State, + strokeWidth: Float, + strokeColor: Color, + error: Boolean, + play: () -> Unit, + pause: () -> Unit, + longClick: () -> Unit, +) { + val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() + if (hasText) { + IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { + Icon( + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), + contentDescription = null, + Modifier.size(36.dp), + tint = MaterialTheme.colors.primary + ) + } + } else { + PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) + } +} + +@Composable +private fun VoiceMsgLoadingProgressIndicator() { + Box( + Modifier + .size(56.dp) + .clip(RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center + ) { + ProgressIndicator() + } +} + +@Composable +private fun FileStatusIcon( + sent: Boolean, + icon: ImageResource, + longClick: () -> Unit, + onClick: () -> Unit, +) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Surface( + color = if (sent) sentColor else receivedColor, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .combinedClickable( + onClick = onClick, + onLongClick = longClick + ) + .onRightClick { longClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(icon), + contentDescription = null, + Modifier.size(36.dp), + tint = MaterialTheme.colors.secondary + ) + } + } +} + @Composable private fun VoiceMsgIndicator( file: CIFile?, @@ -268,39 +343,73 @@ private fun VoiceMsgIndicator( ) { val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } val strokeColor = MaterialTheme.colors.primary - if (file != null && file.loaded && progress != null && duration != null) { - val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() - if (hasText) { - IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { - Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), - contentDescription = null, - Modifier.size(36.dp), - tint = MaterialTheme.colors.primary - ) + when { + file?.fileStatus is CIFileStatus.SndStored -> + if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) { + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + } else { + VoiceMsgLoadingProgressIndicator() } - } else { - PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) - } - } else { - if (file?.fileStatus is CIFileStatus.RcvInvitation) { + file?.fileStatus is CIFileStatus.SndTransfer -> + VoiceMsgLoadingProgressIndicator() + file != null && file.fileStatus is CIFileStatus.SndError -> + FileStatusIcon( + sent, + MR.images.ic_close, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.SndWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file?.fileStatus is CIFileStatus.RcvInvitation -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) - } else if (file?.fileStatus is CIFileStatus.RcvTransfer - || file?.fileStatus is CIFileStatus.RcvAccepted - ) { - Box( - Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } else if (file?.fileStatus is CIFileStatus.RcvAborted) { + file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted -> + VoiceMsgLoadingProgressIndicator() + file?.fileStatus is CIFileStatus.RcvAborted -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) - } else { + file != null && file.fileStatus is CIFileStatus.RcvError -> + FileStatusIcon( + sent, + MR.images.ic_close, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.RcvWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.loaded && progress != null && duration != null -> + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + else -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) - } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index df43d1459f..71218732d6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -271,6 +271,11 @@ Server address is incompatible with network settings. Server version is incompatible with network settings. + + Wrong key or unknown file chunk address - most likely file is deleted. + File not found - most likely file was deleted or cancelled. + File server error: %1$s + Reply Share @@ -408,6 +413,8 @@ Error saving file Loading the file Please, wait while the file is being loaded from the linked mobile + File error + Temporary file error Voice message @@ -1403,6 +1410,8 @@ Database ID Debug delivery Record updated at + Message status + File status Sent at Created at Received at @@ -1411,6 +1420,8 @@ Disappears at Database ID: %d Record updated at: %s + Message status: %s + File status: %s Sent at: %s Created at: %s Received at: %s