core, mobile: file error statuses, cancel sent file (#2193)

This commit is contained in:
spaced4ndy 2023-04-18 12:48:36 +04:00 committed by GitHub
parent 6913bf1a46
commit 09481e09b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 445 additions and 177 deletions

View File

@ -1738,23 +1738,32 @@ class CIFile(
is CIFileStatus.SndTransfer -> true is CIFileStatus.SndTransfer -> true
is CIFileStatus.SndComplete -> true is CIFileStatus.SndComplete -> true
is CIFileStatus.SndCancelled -> true is CIFileStatus.SndCancelled -> true
is CIFileStatus.SndError -> true
is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false
} }
val cancellable: Boolean = when (fileStatus) { val cancelAction: CancelAction? = when (fileStatus) {
is CIFileStatus.SndStored -> fileProtocol != FileProtocol.XFTP // TODO true - enable when XFTP send supports cancel is CIFileStatus.SndStored -> sndCancelAction
is CIFileStatus.SndTransfer -> fileProtocol != FileProtocol.XFTP // TODO true is CIFileStatus.SndTransfer -> sndCancelAction
is CIFileStatus.SndComplete -> false is CIFileStatus.SndComplete ->
is CIFileStatus.SndCancelled -> false if (fileProtocol == FileProtocol.XFTP) {
is CIFileStatus.RcvInvitation -> false revokeCancelAction
is CIFileStatus.RcvAccepted -> true } else {
is CIFileStatus.RcvTransfer -> true null
is CIFileStatus.RcvCancelled -> false }
is CIFileStatus.RcvComplete -> false is CIFileStatus.SndCancelled -> null
is CIFileStatus.SndError -> null
is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null
} }
companion object { companion object {
@ -1769,6 +1778,44 @@ class CIFile(
} }
} }
@Serializable
class CancelAction(
val uiActionId: Int,
val alert: AlertInfo
)
@Serializable
class AlertInfo(
val titleId: Int,
val messageId: Int,
val confirmId: Int
)
private val sndCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_snd_file__title,
messageId = R.string.stop_snd_file__message,
confirmId = R.string.stop_file__confirm
)
)
private val revokeCancelAction: CancelAction = CancelAction(
uiActionId = R.string.revoke_file__action,
alert = AlertInfo(
titleId = R.string.revoke_file__title,
messageId = R.string.revoke_file__message,
confirmId = R.string.revoke_file__confirm
)
)
private val rcvCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_rcv_file__title,
messageId = R.string.stop_rcv_file__message,
confirmId = R.string.stop_file__confirm
)
)
@Serializable @Serializable
enum class FileProtocol { enum class FileProtocol {
@SerialName("smp") SMP, @SerialName("smp") SMP,
@ -1781,11 +1828,13 @@ sealed class CIFileStatus {
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus() @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus() @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus() @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
} }
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")

View File

@ -1442,6 +1442,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
} }
is CR.RcvFileProgressXFTP -> is CR.RcvFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileError -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.SndFileStart -> is CR.SndFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileComplete -> { is CR.SndFileComplete -> {
@ -1458,6 +1462,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem) cleanupFile(r.chatItem)
} }
is CR.SndFileError -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.CallInvitation -> { is CR.CallInvitation -> {
chatModel.callManager.reportNewIncomingCall(r.callInvitation) chatModel.callManager.reportNewIncomingCall(r.callInvitation)
} }
@ -3132,6 +3140,7 @@ sealed class CR {
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR() @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: User, val chatItem: AChatItem): CR()
// sending file events // sending file events
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@ -3139,6 +3148,8 @@ sealed class CR {
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
@Serializable @SerialName("sndFileError") class SndFileError(val user: User, val chatItem: AChatItem): CR()
// call events
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() @Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR() @Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR()
@ -3238,12 +3249,14 @@ sealed class CR {
is RcvFileCancelled -> "rcvFileCancelled" is RcvFileCancelled -> "rcvFileCancelled"
is RcvFileSndCancelled -> "rcvFileSndCancelled" is RcvFileSndCancelled -> "rcvFileSndCancelled"
is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is RcvFileError -> "rcvFileError"
is SndFileCancelled -> "sndFileCancelled" is SndFileCancelled -> "sndFileCancelled"
is SndFileComplete -> "sndFileComplete" is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled" is SndFileRcvCancelled -> "sndFileRcvCancelled"
is SndFileStart -> "sndFileStart" is SndFileStart -> "sndFileStart"
is SndFileProgressXFTP -> "sndFileProgressXFTP" is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP" is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is SndFileError -> "sndFileError"
is CallInvitation -> "callInvitation" is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer" is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer" is CallAnswer -> "callAnswer"
@ -3345,12 +3358,14 @@ sealed class CR {
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem)) is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem)) is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is RcvFileError -> withUser(user, json.encodeToString(chatItem))
is SndFileCancelled -> json.encodeToString(chatItem) is SndFileCancelled -> json.encodeToString(chatItem)
is SndFileComplete -> withUser(user, json.encodeToString(chatItem)) is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem)) is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
is SndFileStart -> withUser(user, json.encodeToString(chatItem)) is SndFileStart -> withUser(user, json.encodeToString(chatItem))
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize") is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is SndFileError -> withUser(user, json.encodeToString(chatItem))
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" 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)}") is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}") is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")

View File

@ -156,6 +156,7 @@ fun CIFileView(
} }
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check) is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.SndError -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvInvitation -> is CIFileStatus.RcvInvitation ->
if (fileSizeValid()) if (fileSizeValid())
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary) fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
@ -170,6 +171,7 @@ fun CIFileView(
} }
is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close) is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvError -> fileIcon(innerIcon = Icons.Outlined.Close)
} }
} else { } else {
fileIcon() fileIcon()

View File

@ -82,10 +82,12 @@ fun CIImageView(
is CIFileStatus.SndTransfer -> progressIndicator() is CIFileStatus.SndTransfer -> progressIndicator()
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_image_snd_complete) is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file) is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image) is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator() is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {} else -> {}
} }
} }

View File

@ -300,6 +300,7 @@ private fun loadingIndicator(file: CIFile?) {
} }
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_video_snd_complete) is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file) is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_video_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video) is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer -> is CIFileStatus.RcvTransfer ->
@ -309,6 +310,7 @@ private fun loadingIndicator(file: CIFile?) {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {} else -> {}
} }
} }

View File

@ -168,8 +168,8 @@ fun ChatItemView(
} }
) )
} }
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancellable) { if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile) CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
} }
if (!(live && cItem.meta.isLive)) { if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
@ -278,14 +278,15 @@ fun ChatItemView(
fun CancelFileItemAction( fun CancelFileItemAction(
fileId: Long, fileId: Long,
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit cancelFile: (Long) -> Unit,
cancelAction: CancelAction
) { ) {
ItemAction( ItemAction(
stringResource(R.string.cancel_verb), stringResource(cancelAction.uiActionId),
Icons.Outlined.Close, Icons.Outlined.Close,
onClick = { onClick = {
showMenu.value = false showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile) cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
}, },
color = Color.Red color = Color.Red
) )
@ -344,11 +345,11 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
} }
} }
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit) { fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.cancel_file__question), title = generalGetString(cancelAction.alert.titleId),
text = generalGetString(R.string.file_transfer_will_be_cancelled_warning), text = generalGetString(cancelAction.alert.messageId),
confirmText = generalGetString(R.string.confirm_verb), confirmText = generalGetString(cancelAction.alert.confirmId),
destructive = true, destructive = true,
onConfirm = { onConfirm = {
cancelFile(fileId) cancelFile(fileId)

View File

@ -218,10 +218,18 @@
<string name="delete_member_message__question">Delete member message?</string> <string name="delete_member_message__question">Delete member message?</string>
<string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string> <string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string>
<string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string> <string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string>
<string name="cancel_file__question">Cancel file transfer?</string>
<string name="file_transfer_will_be_cancelled_warning">File transfer will be cancelled. If it\'s in progress it will be stoppped.</string>
<string name="for_me_only">Delete for me</string> <string name="for_me_only">Delete for me</string>
<string name="for_everybody">For everyone</string> <string name="for_everybody">For everyone</string>
<string name="stop_file__action">Stop file</string>
<string name="stop_snd_file__title">Stop sending file?</string>
<string name="stop_snd_file__message">Sending file will be stopped.</string>
<string name="stop_rcv_file__title">Stop receiving file?</string>
<string name="stop_rcv_file__message">Receiving file will be stopped.</string>
<string name="stop_file__confirm">Stop</string>
<string name="revoke_file__action">Revoke file</string>
<string name="revoke_file__title">Revoke file?</string>
<string name="revoke_file__message">File will be deleted from servers.</string>
<string name="revoke_file__confirm">Revoke</string>
<!-- CIMetaView.kt --> <!-- CIMetaView.kt -->
<string name="icon_descr_edited">edited</string> <string name="icon_descr_edited">edited</string>

View File

@ -1333,6 +1333,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
cleanupFile(aChatItem) cleanupFile(aChatItem)
case let .rcvFileProgressXFTP(user, aChatItem, _, _): case let .rcvFileProgressXFTP(user, aChatItem, _, _):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .sndFileStart(user, aChatItem, _): case let .sndFileStart(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _): case let .sndFileComplete(user, aChatItem, _):
@ -1346,6 +1349,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .sndFileCompleteXFTP(user, aChatItem, _): case let .sndFileCompleteXFTP(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem) cleanupFile(aChatItem)
case let .sndFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .callInvitation(invitation): case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation) activateCall(invitation)

View File

@ -55,11 +55,13 @@ struct CIFileView: View {
case .sndTransfer: return false case .sndTransfer: return false
case .sndComplete: return false case .sndComplete: return false
case .sndCancelled: return false case .sndCancelled: return false
case .sndError: return false
case .rcvInvitation: return true case .rcvInvitation: return true
case .rcvAccepted: return true case .rcvAccepted: return true
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvComplete: return true case .rcvComplete: return true
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvError: return false
} }
} }
return false return false
@ -130,6 +132,7 @@ struct CIFileView: View {
} }
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10) case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvInvitation: case .rcvInvitation:
if fileSizeValid() { if fileSizeValid() {
fileIcon("arrow.down.doc.fill", color: .accentColor) fileIcon("arrow.down.doc.fill", color: .accentColor)
@ -145,6 +148,7 @@ struct CIFileView: View {
} }
case .rcvComplete: fileIcon("doc.fill") case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
} }
} else { } else {
fileIcon("doc.fill") fileIcon("doc.fill")

View File

@ -93,10 +93,12 @@ struct CIImageView: View {
case .sndTransfer: progressView() case .sndTransfer: progressView()
case .sndComplete: fileIcon("checkmark", 10, 13) case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: fileIcon("xmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13)
case .sndError: fileIcon("xmark", 10, 13)
case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvAccepted: fileIcon("ellipsis", 14, 11)
case .rcvTransfer: progressView() case .rcvTransfer: progressView()
case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
default: EmptyView() default: EmptyView()
} }
} }

View File

@ -199,38 +199,33 @@ struct CIVideoView: View {
case .xftp: progressCircle(sndProgress, sndTotal) case .xftp: progressCircle(sndProgress, sndTotal)
case .smp: progressView() case .smp: progressView()
} }
case .sndComplete: case .sndComplete: fileIcon("checkmark", 10, 13)
Image(systemName: "checkmark") case .sndCancelled: fileIcon("xmark", 10, 13)
.resizable() case .sndError: fileIcon("xmark", 10, 13)
.aspectRatio(contentMode: .fit) case .rcvInvitation: fileIcon("arrow.down", 10, 13)
.frame(width: 10, height: 10) case .rcvAccepted: fileIcon("ellipsis", 14, 11)
.foregroundColor(.white)
.padding(13)
case .rcvInvitation:
Image(systemName: "arrow.down")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.foregroundColor(.white)
.padding(11)
case .rcvAccepted:
Image(systemName: "ellipsis")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.foregroundColor(.white)
.padding(11)
case let .rcvTransfer(rcvProgress, rcvTotal): case let .rcvTransfer(rcvProgress, rcvTotal):
if file.fileProtocol == .xftp && rcvProgress < rcvTotal { if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
progressCircle(rcvProgress, rcvTotal) progressCircle(rcvProgress, rcvTotal)
} else { } else {
progressView() progressView()
} }
case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
default: EmptyView() default: EmptyView()
} }
} }
} }
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(padding)
}
private func progressView() -> some View { private func progressView() -> some View {
ProgressView() ProgressView()
.progressViewStyle(.circular) .progressViewStyle(.circular)

View File

@ -108,11 +108,13 @@ struct VoiceMessagePlayer: View {
case .sndTransfer: playbackButton() case .sndTransfer: playbackButton()
case .sndComplete: playbackButton() case .sndComplete: playbackButton()
case .sndCancelled: playbackButton() case .sndCancelled: playbackButton()
case .sndError: playbackButton()
case .rcvInvitation: loadingIcon() case .rcvInvitation: loadingIcon()
case .rcvAccepted: loadingIcon() case .rcvAccepted: loadingIcon()
case .rcvTransfer: loadingIcon() case .rcvTransfer: loadingIcon()
case .rcvComplete: playbackButton() case .rcvComplete: playbackButton()
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
} }
} else { } else {
playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))

View File

@ -496,8 +496,8 @@ struct ChatView: View {
} }
if ci.meta.itemDeleted == nil, if ci.meta.itemDeleted == nil,
let file = ci.file, let file = ci.file,
file.cancellable { let cancelAction = file.cancelAction {
menu.append(cancelFileUIAction(file.fileId, sent: ci.chatDir.sent)) menu.append(cancelFileUIAction(file.fileId, cancelAction))
} }
if !live || !ci.meta.isLive { if !live || !ci.meta.isLive {
menu.append(deleteUIAction()) menu.append(deleteUIAction())
@ -589,15 +589,16 @@ struct ChatView: View {
} }
} }
private func cancelFileUIAction(_ fileId: Int64, sent: Bool) -> UIAction { private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction {
UIAction( return UIAction(
title: NSLocalizedString("Stop file", comment: "chat item action"), title: cancelAction.uiAction,
image: UIImage(systemName: "xmark") image: UIImage(systemName: "xmark"),
attributes: [.destructive]
) { _ in ) { _ in
AlertManager.shared.showAlert(Alert( AlertManager.shared.showAlert(Alert(
title: Text(sent ? "Stop sending file?" : "Stop receiving file?"), title: Text(cancelAction.alert.title),
message: Text(sent ? "Sending file will be stopped." : "Receiving file will be stopped."), message: Text(cancelAction.alert.message),
primaryButton: .destructive(Text("Stop")) { primaryButton: .destructive(Text(cancelAction.alert.confirm)) {
Task { Task {
if let user = ChatModel.shared.currentUser { if let user = ChatModel.shared.currentUser {
await cancelFile(user: user, fileId: fileId) await cancelFile(user: user, fileId: fileId)

View File

@ -453,6 +453,7 @@ public enum ChatResponse: Decodable, Error {
case rcvFileComplete(user: User, chatItem: AChatItem) case rcvFileComplete(user: User, chatItem: AChatItem)
case rcvFileCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) case rcvFileCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
case rcvFileSndCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) case rcvFileSndCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
case rcvFileError(user: User, chatItem: AChatItem)
// sending file events // sending file events
case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileComplete(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
@ -460,6 +461,8 @@ public enum ChatResponse: Decodable, Error {
case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) case sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case sndFileCompleteXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) case sndFileCompleteXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
case sndFileError(user: User, chatItem: AChatItem)
// call events
case callInvitation(callInvitation: RcvCallInvitation) case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(user: User, contact: Contact, answer: WebRTCSession) case callAnswer(user: User, contact: Contact, answer: WebRTCSession)
@ -564,12 +567,14 @@ public enum ChatResponse: Decodable, Error {
case .rcvFileComplete: return "rcvFileComplete" case .rcvFileComplete: return "rcvFileComplete"
case .rcvFileCancelled: return "rcvFileCancelled" case .rcvFileCancelled: return "rcvFileCancelled"
case .rcvFileSndCancelled: return "rcvFileSndCancelled" case .rcvFileSndCancelled: return "rcvFileSndCancelled"
case .rcvFileError: return "rcvFileError"
case .sndFileStart: return "sndFileStart" case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete" case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled" case .sndFileCancelled: return "sndFileCancelled"
case .sndFileRcvCancelled: return "sndFileRcvCancelled" case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndFileProgressXFTP: return "sndFileProgressXFTP" case .sndFileProgressXFTP: return "sndFileProgressXFTP"
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP" case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
case .sndFileError: return "sndFileError"
case .callInvitation: return "callInvitation" case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer" case .callOffer: return "callOffer"
case .callAnswer: return "callAnswer" case .callAnswer: return "callAnswer"
@ -677,12 +682,14 @@ public enum ChatResponse: Decodable, Error {
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .rcvFileError(u, chatItem): return withUser(u, String(describing: chatItem))
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem))
case let .callInvitation(inv): return String(describing: inv) case let .callInvitation(inv): return String(describing: inv)
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")

View File

@ -2281,32 +2281,79 @@ public struct CIFile: Decodable {
case .sndTransfer: return true case .sndTransfer: return true
case .sndComplete: return true case .sndComplete: return true
case .sndCancelled: return true case .sndCancelled: return true
case .sndError: return true
case .rcvInvitation: return false case .rcvInvitation: return false
case .rcvAccepted: return false case .rcvAccepted: return false
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvComplete: return true case .rcvComplete: return true
case .rcvError: return false
} }
} }
} }
public var cancellable: Bool { public var cancelAction: CancelAction? {
get { get {
switch self.fileStatus { switch self.fileStatus {
case .sndStored: return self.fileProtocol != .xftp // TODO true - enable when XFTP send supports cancel case .sndStored: return sndCancelAction
case .sndTransfer: return self.fileProtocol != .xftp // TODO true case .sndTransfer: return sndCancelAction
case .sndComplete: return false case .sndComplete:
case .sndCancelled: return false if self.fileProtocol == .xftp {
case .rcvInvitation: return false return revokeCancelAction
case .rcvAccepted: return true } else {
case .rcvTransfer: return true return nil
case .rcvCancelled: return false }
case .rcvComplete: return false case .sndCancelled: return nil
case .sndError: return nil
case .rcvInvitation: return nil
case .rcvAccepted: return rcvCancelAction
case .rcvTransfer: return rcvCancelAction
case .rcvCancelled: return nil
case .rcvComplete: return nil
case .rcvError: return nil
} }
} }
} }
} }
public struct CancelAction {
public var uiAction: String
public var alert: AlertInfo
}
public struct AlertInfo {
public var title: LocalizedStringKey
public var message: LocalizedStringKey
public var confirm: LocalizedStringKey
}
private var sndCancelAction = CancelAction(
uiAction: NSLocalizedString("Stop file", comment: "cancel file action"),
alert: AlertInfo(
title: "Stop sending file?",
message: "Sending file will be stopped.",
confirm: "Stop"
)
)
private var revokeCancelAction = CancelAction(
uiAction: NSLocalizedString("Revoke file", comment: "cancel file action"),
alert: AlertInfo(
title: "Revoke file?",
message: "File will be deleted from servers.",
confirm: "Revoke"
)
)
private var rcvCancelAction = CancelAction(
uiAction: NSLocalizedString("Stop file", comment: "cancel file action"),
alert: AlertInfo(
title: "Stop receiving file?",
message: "Receiving file will be stopped.",
confirm: "Stop"
)
)
public enum FileProtocol: String, Decodable { public enum FileProtocol: String, Decodable {
case smp = "smp" case smp = "smp"
case xftp = "xftp" case xftp = "xftp"
@ -2317,11 +2364,13 @@ public enum CIFileStatus: Decodable {
case sndTransfer(sndProgress: Int64, sndTotal: Int64) case sndTransfer(sndProgress: Int64, sndTotal: Int64)
case sndComplete case sndComplete
case sndCancelled case sndCancelled
case sndError
case rcvInvitation case rcvInvitation
case rcvAccepted case rcvAccepted
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
case rcvComplete case rcvComplete
case rcvCancelled case rcvCancelled
case rcvError
var id: String { var id: String {
switch self { switch self {
@ -2329,11 +2378,13 @@ public enum CIFileStatus: Decodable {
case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)" case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
case .sndComplete: return "sndComplete" case .sndComplete: return "sndComplete"
case .sndCancelled: return "sndCancelled" case .sndCancelled: return "sndCancelled"
case .sndError: return "sndError"
case .rcvInvitation: return "rcvInvitation" case .rcvInvitation: return "rcvInvitation"
case .rcvAccepted: return "rcvAccepted" case .rcvAccepted: return "rcvAccepted"
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
case .rcvComplete: return "rcvComplete" case .rcvComplete: return "rcvComplete"
case .rcvCancelled: return "rcvCancelled" case .rcvCancelled: return "rcvCancelled"
case .rcvError: return "rcvError"
} }
} }
} }

View File

@ -1395,9 +1395,9 @@ processChatCommand = \case
CancelFile fileId -> withUser $ \user@User {userId} -> CancelFile fileId -> withUser $ \user@User {userId} ->
withChatLock "cancelFile" . procCmd $ withChatLock "cancelFile" . procCmd $
withStore (\db -> getFileTransfer db user fileId) >>= \case withStore (\db -> getFileTransfer db user fileId) >>= \case
FTSnd ftm@FileTransferMeta {cancelled} fts FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| not (null fts) && all (\SndFileTransfer {fileStatus = s} -> s == FSComplete || s == FSCancelled) fts -> | not (null fts) && all fileCancelledOrCompleteSMP fts ->
throwChatError $ CEFileCancel fileId "file transfer is complete" throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do | otherwise -> do
fileAgentConnIds <- cancelSndFile user ftm fts True fileAgentConnIds <- cancelSndFile user ftm fts True
@ -1413,6 +1413,9 @@ processChatCommand = \case
_ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> getChatItemByFileId db user fileId
pure $ CRSndFileCancelled user ci ftm fts pure $ CRSndFileCancelled user ci ftm fts
where
fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} =
s == FSCancelled || (s == FSComplete && isNothing xftpSndFile)
FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile} FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile}
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete" | rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete"
@ -2333,62 +2336,63 @@ processAgentMsgSndFile _corrId aFileId msg =
(ft@FileTransferMeta {fileId, cancelled}, sfts) <- withStore $ \db -> do (ft@FileTransferMeta {fileId, cancelled}, sfts) <- withStore $ \db -> do
fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId
getSndFileTransfer db user fileId getSndFileTransfer db user fileId
case msg of unless cancelled $ case msg of
SFPROG sndProgress sndTotal -> SFPROG sndProgress sndTotal -> do
unless cancelled $ do let status = CIFSSndTransfer {sndProgress, sndTotal}
let status = CIFSSndTransfer {sndProgress, sndTotal} ci <- withStore $ \db -> do
liftIO $ updateCIFileStatus db user fileId status
getChatItemByFileId db user fileId
toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE sndDescr rfds -> do
withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr)
ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <-
withStore $ \db -> getChatItemByFileId db user fileId
case (msgId_, itemDeleted) of
(Just sharedMsgId, Nothing) -> do
when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
-- TODO either update database status or move to SFPROG
toView $ CRSndFileProgressXFTP user ci ft 1 1
case (rfds, sfts, d, cInfo) of
(rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
agentXFTPDeleteSndFileInternal user aFileId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g
let rfdsMemberFTs = zip rfds $ memberFTs ms
extraRFDs = drop (length rfdsMemberFTs) rfds
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
forM_ rfdsMemberFTs $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
ci' <- withStore $ \db -> do
liftIO $ updateCIFileStatus db user fileId CIFSSndComplete
getChatItemByFileId db user fileId
agentXFTPDeleteSndFileInternal user aFileId
toView $ CRSndFileCompleteXFTP user ci' ft
where
memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)]
memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts')
where
mConns' = mapMaybe useMember ms
sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts
useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}}
| (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn)
| otherwise = Nothing
useMember _ = Nothing
sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m ()
sendToMember (rfd, (conn, sft)) =
void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
_ -> pure ()
_ -> pure () -- TODO error?
SFERR e
| temporaryAgentError e ->
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e
| otherwise -> do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
liftIO $ updateCIFileStatus db user fileId status liftIO $ updateFileCancelled db user fileId CIFSSndError
getChatItemByFileId db user fileId getChatItemByFileId db user fileId
toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE sndDescr rfds ->
unless cancelled $ do
withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr)
ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <-
withStore $ \db -> getChatItemByFileId db user fileId
case (msgId_, itemDeleted) of
(Just sharedMsgId, Nothing) -> do
when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
-- TODO either update database status or move to SFPROG
toView $ CRSndFileProgressXFTP user ci ft 1 1
case (rfds, sfts, d, cInfo) of
(rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
agentXFTPDeleteSndFileInternal user aFileId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g
let rfdsMemberFTs = zip rfds $ memberFTs ms
extraRFDs = drop (length rfdsMemberFTs) rfds
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs)
forM_ rfdsMemberFTs $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
ci' <- withStore $ \db -> do
liftIO $ updateCIFileStatus db user fileId CIFSSndComplete
getChatItemByFileId db user fileId
agentXFTPDeleteSndFileInternal user aFileId
toView $ CRSndFileCompleteXFTP user ci' ft
where
memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)]
memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts')
where
mConns' = mapMaybe useMember ms
sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts
useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}}
| (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn)
| otherwise = Nothing
useMember _ = Nothing
sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m ()
sendToMember (rfd, (conn, sft)) =
void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
_ -> pure ()
_ -> pure () -- TODO error?
SFERR e -> do
unless (temporaryAgentError e) $ do
-- update chat item status
-- send status to view
agentXFTPDeleteSndFileInternal user aFileId agentXFTPDeleteSndFileInternal user aFileId
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e toView $ CRSndFileError user ci
where where
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
fileDescrText = safeDecodeUtf8 . strEncode fileDescrText = safeDecodeUtf8 . strEncode
@ -2416,37 +2420,38 @@ processAgentMsgRcvFile _corrId aFileId msg =
where where
process :: User -> m () process :: User -> m ()
process user = do process user = do
ft@RcvFileTransfer {fileId, cancelled} <- withStore $ \db -> do ft@RcvFileTransfer {fileId} <- withStore $ \db -> do
fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId
getRcvFileTransfer db user fileId getRcvFileTransfer db user fileId
case msg of unless (rcvFileCompleteOrCancelled ft) $ case msg of
RFPROG rcvProgress rcvTotal -> RFPROG rcvProgress rcvTotal -> do
unless cancelled $ do let status = CIFSRcvTransfer {rcvProgress, rcvTotal}
let status = CIFSRcvTransfer {rcvProgress, rcvTotal} ci <- withStore $ \db -> do
ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId status
liftIO $ updateCIFileStatus db user fileId status getChatItemByFileId db user fileId
getChatItemByFileId db user fileId toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
RFDONE xftpPath -> RFDONE xftpPath ->
unless cancelled $ do case liveRcvFileTransferPath ft of
case liveRcvFileTransferPath ft of Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file" Just targetPath -> do
Just targetPath -> do fsTargetPath <- toFSFilePath targetPath
fsTargetPath <- toFSFilePath targetPath renameFile xftpPath fsTargetPath
renameFile xftpPath fsTargetPath ci <- withStore $ \db -> do
ci <- withStore $ \db -> do liftIO $ do
liftIO $ do updateRcvFileStatus db fileId FSComplete
updateRcvFileStatus db fileId FSComplete updateCIFileStatus db user fileId CIFSRcvComplete
updateCIFileStatus db user fileId CIFSRcvComplete getChatItemByFileId db user fileId
getChatItemByFileId db user fileId agentXFTPDeleteRcvFile user aFileId fileId
agentXFTPDeleteRcvFile user aFileId fileId toView $ CRRcvFileComplete user ci
toView $ CRRcvFileComplete user ci RFERR e
RFERR e -> do | temporaryAgentError e ->
unless (temporaryAgentError e) $ do throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
-- update chat item status | otherwise -> do
-- send status to view ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId CIFSRcvError
getChatItemByFileId db user fileId
agentXFTPDeleteRcvFile user aFileId fileId agentXFTPDeleteRcvFile user aFileId fileId
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e toView $ CRRcvFileError user ci
processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m () processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessageConn user _ agentConnId END = processAgentMessageConn user _ agentConnId END =
@ -2940,9 +2945,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> pure () _ -> pure ()
receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m () receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m ()
receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize, cancelled} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case
FileChunkCancel -> FileChunkCancel ->
unless cancelled $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft toView $ CRRcvFileSndCancelled user ci ft
@ -3082,8 +3087,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err
badRcvFileChunk :: RcvFileTransfer -> String -> m () badRcvFileChunk :: RcvFileTransfer -> String -> m ()
badRcvFileChunk ft@RcvFileTransfer {cancelled} err = badRcvFileChunk ft err =
unless cancelled $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
throwChatError $ CEFileRcvChunk err throwChatError $ CEFileRcvChunk err
@ -3165,14 +3170,14 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processFDMessage :: FileTransferId -> FileDescr -> m () processFDMessage :: FileTransferId -> FileDescr -> m ()
processFDMessage fileId fileDescr = do processFDMessage fileId fileDescr = do
RcvFileTransfer {cancelled} <- withStore $ \db -> getRcvFileTransfer db user fileId ft <- withStore $ \db -> getRcvFileTransfer db user fileId
unless cancelled $ do unless (rcvFileCompleteOrCancelled ft) $ do
(rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do (rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do
rfd <- appendRcvFD db userId fileId fileDescr rfd <- appendRcvFD db userId fileId fileDescr
-- reading second time in the same transaction as appending description -- reading second time in the same transaction as appending description
-- to prevent race condition with accept -- to prevent race condition with accept
ft <- getRcvFileTransfer db user fileId ft' <- getRcvFileTransfer db user fileId
pure (rfd, ft) pure (rfd, ft')
case fileStatus of case fileStatus of
RFSAccepted _ -> receiveViaCompleteFD user fileId rfd RFSAccepted _ -> receiveViaCompleteFD user fileId rfd
_ -> pure () _ -> pure ()
@ -3365,8 +3370,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless cancelled $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft toView $ CRRcvFileSndCancelled user ci ft
@ -3446,8 +3451,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(SMDRcv, CIGroupRcv m) -> do (SMDRcv, CIGroupRcv m) -> do
if sameMemberId memberId m if sameMemberId memberId m
then do then do
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless cancelled $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> getChatItemByFileId db user fileId
toView $ CRRcvFileSndCancelled user ci ft toView $ CRRcvFileSndCancelled user ci ft

View File

@ -438,6 +438,7 @@ data ChatResponse
| CRRcvFileComplete {user :: User, chatItem :: AChatItem} | CRRcvFileComplete {user :: User, chatItem :: AChatItem}
| CRRcvFileCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileError {user :: User, chatItem :: AChatItem}
| CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
@ -446,6 +447,7 @@ data ChatResponse
| CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64} | CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64}
| CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRSndFileError {user :: User, chatItem :: AChatItem}
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile} | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile}
| CRContactAliasUpdated {user :: User, toContact :: Contact} | CRContactAliasUpdated {user :: User, toContact :: Contact}
| CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection}

View File

@ -448,11 +448,13 @@ data CIFileStatus (d :: MsgDirection) where
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
CIFSSndCancelled :: CIFileStatus 'MDSnd CIFSSndCancelled :: CIFileStatus 'MDSnd
CIFSSndComplete :: CIFileStatus 'MDSnd CIFSSndComplete :: CIFileStatus 'MDSnd
CIFSSndError :: CIFileStatus 'MDSnd
CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv
CIFSRcvError :: CIFileStatus 'MDRcv
deriving instance Eq (CIFileStatus d) deriving instance Eq (CIFileStatus d)
@ -464,11 +466,13 @@ ciFileEnded = \case
CIFSSndTransfer {} -> False CIFSSndTransfer {} -> False
CIFSSndCancelled -> True CIFSSndCancelled -> True
CIFSSndComplete -> True CIFSSndComplete -> True
CIFSSndError -> True
CIFSRcvInvitation -> False CIFSRcvInvitation -> False
CIFSRcvAccepted -> False CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False CIFSRcvTransfer {} -> False
CIFSRcvCancelled -> True CIFSRcvCancelled -> True
CIFSRcvComplete -> True CIFSRcvComplete -> True
CIFSRcvError -> True
instance ToJSON (CIFileStatus d) where instance ToJSON (CIFileStatus d) where
toJSON = J.toJSON . jsonCIFileStatus toJSON = J.toJSON . jsonCIFileStatus
@ -488,11 +492,13 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total) CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
CIFSSndCancelled -> "snd_cancelled" CIFSSndCancelled -> "snd_cancelled"
CIFSSndComplete -> "snd_complete" CIFSSndComplete -> "snd_complete"
CIFSSndError -> "snd_error"
CIFSRcvInvitation -> "rcv_invitation" CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted" CIFSRcvAccepted -> "rcv_accepted"
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvComplete -> "rcv_complete" CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled" CIFSRcvCancelled -> "rcv_cancelled"
CIFSRcvError -> "rcv_error"
strP = (\(AFS _ st) -> checkDirection st) <$?> strP strP = (\(AFS _ st) -> checkDirection st) <$?> strP
instance StrEncoding ACIFileStatus where instance StrEncoding ACIFileStatus where
@ -503,11 +509,13 @@ instance StrEncoding ACIFileStatus where
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer "snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled "snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete "snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
"snd_error" -> pure $ AFS SMDSnd CIFSSndError
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
_ -> fail "bad file status" _ -> fail "bad file status"
where where
progress :: (Int64 -> Int64 -> a) -> A.Parser a progress :: (Int64 -> Int64 -> a) -> A.Parser a
@ -519,11 +527,13 @@ data JSONCIFileStatus
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64} | JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
| JCIFSSndCancelled | JCIFSSndCancelled
| JCIFSSndComplete | JCIFSSndComplete
| JCIFSSndError
| JCIFSRcvInvitation | JCIFSRcvInvitation
| JCIFSRcvAccepted | JCIFSRcvAccepted
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvComplete | JCIFSRcvComplete
| JCIFSRcvCancelled | JCIFSRcvCancelled
| JCIFSRcvError
deriving (Generic) deriving (Generic)
instance ToJSON JSONCIFileStatus where instance ToJSON JSONCIFileStatus where
@ -536,11 +546,13 @@ jsonCIFileStatus = \case
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
CIFSSndCancelled -> JCIFSSndCancelled CIFSSndCancelled -> JCIFSSndCancelled
CIFSSndComplete -> JCIFSSndComplete CIFSSndComplete -> JCIFSSndComplete
CIFSSndError -> JCIFSSndError
CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvInvitation -> JCIFSRcvInvitation
CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvAccepted -> JCIFSRcvAccepted
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvComplete -> JCIFSRcvComplete
CIFSRcvCancelled -> JCIFSRcvCancelled CIFSRcvCancelled -> JCIFSRcvCancelled
CIFSRcvError -> JCIFSRcvError
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
aciFileStatusJSON = \case aciFileStatusJSON = \case
@ -548,11 +560,13 @@ aciFileStatusJSON = \case
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
JCIFSSndError -> AFS SMDSnd CIFSSndError
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
-- to conveniently read file data from db -- to conveniently read file data from db
data CIFileInfo = CIFileInfo data CIFileInfo = CIFileInfo

View File

@ -1652,6 +1652,9 @@ rcvFileComplete = \case
RFSComplete _ -> True RFSComplete _ -> True
_ -> False _ -> False
rcvFileCompleteOrCancelled :: RcvFileTransfer -> Bool
rcvFileCompleteOrCancelled RcvFileTransfer {fileStatus, cancelled} = rcvFileComplete fileStatus || cancelled
data RcvFileInfo = RcvFileInfo data RcvFileInfo = RcvFileInfo
{ filePath :: FilePath, { filePath :: FilePath,
connId :: Maybe Int64, connId :: Maybe Int64,

View File

@ -153,12 +153,14 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
CRRcvFileError u ci -> ttyUser u $ receivingFile_' "error" ci
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
CRSndFileStartXFTP _ _ _ -> [] CRSndFileStartXFTP {} -> []
CRSndFileProgressXFTP _ _ _ _ _ -> [] CRSndFileProgressXFTP {} -> []
CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadedFile ci CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci
CRSndFileCancelledXFTP _ _ _ -> [] CRSndFileCancelledXFTP {} -> []
CRSndFileError u ci -> ttyUser u $ uploadingFile "error" ci
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
CRContactConnecting u _ -> ttyUser u [] CRContactConnecting u _ -> ttyUser u []
@ -1074,12 +1076,12 @@ sendingFile_ :: StyledString -> SndFileTransfer -> [StyledString]
sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} = sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} =
[status <> " sending " <> sndFile ft <> " to " <> ttyContact c] [status <> " sending " <> sndFile ft <> " to " <> ttyContact c]
uploadedFile :: AChatItem -> [StyledString] uploadingFile :: StyledString -> AChatItem -> [StyledString]
uploadedFile (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) = uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) =
["uploaded " <> fileTransferStr fileId fileName <> " for " <> ttyContact c] [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c]
uploadedFile (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) = uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) =
["uploaded " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g] [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g]
uploadedFile _ = ["uploaded file"] -- shouldn't happen uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen
sndFile :: SndFileTransfer -> StyledString sndFile :: SndFileTransfer -> StyledString
sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName

View File

@ -343,11 +343,14 @@ xftpServerConfig =
} }
withXFTPServer :: IO () -> IO () withXFTPServer :: IO () -> IO ()
withXFTPServer = withXFTPServer = withXFTPServer' xftpServerConfig
withXFTPServer' :: XFTPServerConfig -> IO () -> IO ()
withXFTPServer' cfg =
serverBracket serverBracket
( \started -> do ( \started -> do
createDirectoryIfMissing False xftpServerFiles createDirectoryIfMissing False xftpServerFiles
runXFTPServerBlocking started xftpServerConfig runXFTPServerBlocking started cfg
) )
serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO () serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO ()

View File

@ -12,6 +12,7 @@ import Simplex.Chat (roundedFDCount)
import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig) import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig)
import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Options (ChatOpts (..))
import Simplex.FileTransfer.Client.Main (xftpClientCLI) import Simplex.FileTransfer.Client.Main (xftpClientCLI)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
import Simplex.Messaging.Util (unlessM) import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, doesFileExist) import System.Directory (copyFile, doesFileExist)
import System.Environment (withArgs) import System.Environment (withArgs)
@ -59,9 +60,12 @@ chatFileTests = do
it "send and receive file" testXFTPFileTransfer it "send and receive file" testXFTPFileTransfer
it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload
it "send and receive file in group" testXFTPGroupFileTransfer it "send and receive file in group" testXFTPGroupFileTransfer
it "delete uploaded file" testXFTPDeleteUploadedFile
it "delete uploaded file in group" testXFTPDeleteUploadedFileGroup
it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig
it "with relative paths: send and receive file" testXFTPWithRelativePaths it "with relative paths: send and receive file" testXFTPWithRelativePaths
xit' "continue receiving file after restart" testXFTPContinueRcv xit' "continue receiving file after restart" testXFTPContinueRcv
it "error receiving file" testXFTPRcvError
it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat
runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO () runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
@ -125,9 +129,7 @@ testAcceptInlineFileSndCancelDuringTransfer =
[ do [ do
alice <##. "cancelled sending file 1 (test_1MB.pdf)" alice <##. "cancelled sending file 1 (test_1MB.pdf)"
alice <## "completed sending file 1 (test_1MB.pdf) to bob", alice <## "completed sending file 1 (test_1MB.pdf) to bob",
do bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
bob <## "alice cancelled sending file 1 (test_1MB.pdf)"
] ]
alice #> "@bob hi" alice #> "@bob hi"
bob <# "alice> hi" bob <# "alice> hi"
@ -988,7 +990,7 @@ testXFTPFileTransfer =
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for bob" alice <## "completed uploading file 1 (test.pdf) for bob"
bob <## "started receiving file 1 (test.pdf) from alice" bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "completed receiving file 1 (test.pdf) from alice" bob <## "completed receiving file 1 (test.pdf) from alice"
@ -1009,7 +1011,7 @@ testXFTPAcceptAfterUpload =
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for bob" alice <## "completed uploading file 1 (test.pdf) for bob"
threadDelay 100000 threadDelay 100000
@ -1041,7 +1043,7 @@ testXFTPGroupFileTransfer =
cath <## "use /fr 1 [<dir>/ | <path>] to receive it" cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
] ]
-- alice <## "started sending file 1 (test.pdf) to #team" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to #team" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for #team" alice <## "completed uploading file 1 (test.pdf) for #team"
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob bob
@ -1065,6 +1067,71 @@ testXFTPGroupFileTransfer =
where where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPDeleteUploadedFile :: HasCallStack => FilePath -> IO ()
testXFTPDeleteUploadedFile =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
withXFTPServer $ do
connectUsers alice bob
alice #> "/f @bob ./tests/fixtures/test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "completed uploading file 1 (test.pdf) for bob"
alice ##> "/fc 1"
concurrentlyN_
[ alice <## "cancelled sending file 1 (test.pdf)",
bob <## "alice cancelled sending file 1 (test.pdf)"
]
bob ##> "/fr 1 ./tests/tmp"
bob <## "file cancelled: test.pdf"
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPDeleteUploadedFileGroup :: HasCallStack => FilePath -> IO ()
testXFTPDeleteUploadedFileGroup =
testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do
withXFTPServer $ do
createGroup3 "team" alice bob cath
alice #> "/f #team ./tests/fixtures/test.pdf"
alice <## "use /fc 1 to cancel sending"
concurrentlyN_
[ do
bob <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it",
do
cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
]
-- alice <## "started sending file 1 (test.pdf) to #team" -- TODO "started uploading" ?
alice <## "completed uploading file 1 (test.pdf) for #team"
bob ##> "/fr 1 ./tests/tmp"
bob
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
"started receiving file 1 (test.pdf) from alice"
]
bob <## "completed receiving file 1 (test.pdf) from alice"
src <- B.readFile "./tests/fixtures/test.pdf"
dest <- B.readFile "./tests/tmp/test.pdf"
dest `shouldBe` src
alice ##> "/fc 1"
concurrentlyN_
[ alice <## "cancelled sending file 1 (test.pdf) to bob, cath",
cath <## "alice cancelled sending file 1 (test.pdf)"
]
cath ##> "/fr 1 ./tests/tmp"
cath <## "file cancelled: test.pdf"
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPWithChangedConfig :: HasCallStack => FilePath -> IO () testXFTPWithChangedConfig :: HasCallStack => FilePath -> IO ()
testXFTPWithChangedConfig = testXFTPWithChangedConfig =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
@ -1084,7 +1151,7 @@ testXFTPWithChangedConfig =
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for bob" alice <## "completed uploading file 1 (test.pdf) for bob"
bob <## "started receiving file 1 (test.pdf) from alice" bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "completed receiving file 1 (test.pdf) from alice" bob <## "completed receiving file 1 (test.pdf) from alice"
@ -1123,7 +1190,7 @@ testXFTPWithRelativePaths =
bob ##> "/fr 1" bob ##> "/fr 1"
bob <## "saving file 1 from alice to test.pdf" bob <## "saving file 1 from alice to test.pdf"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for bob" alice <## "completed uploading file 1 (test.pdf) for bob"
bob <## "started receiving file 1 (test.pdf) from alice" bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "completed receiving file 1 (test.pdf) from alice" bob <## "completed receiving file 1 (test.pdf) from alice"
@ -1145,7 +1212,7 @@ testXFTPContinueRcv tmp = do
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (test.pdf) for bob" alice <## "completed uploading file 1 (test.pdf) for bob"
-- server is down - file is not received -- server is down - file is not received
withTestChatCfg tmp cfg "bob" $ \bob -> do withTestChatCfg tmp cfg "bob" $ \bob -> do
@ -1166,6 +1233,31 @@ testXFTPContinueRcv tmp = do
where where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPRcvError :: HasCallStack => FilePath -> IO ()
testXFTPRcvError tmp = do
withXFTPServer $ do
withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
connectUsers alice bob
alice #> "/f @bob ./tests/fixtures/test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "completed uploading file 1 (test.pdf) for bob"
-- server is up w/t store log - file reception should fail
withXFTPServer' xftpServerConfig {storeLogFile = Nothing} $ do
withTestChatCfg tmp cfg "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob ##> "/fr 1 ./tests/tmp"
bob <## "started receiving file 1 (test.pdf) from alice"
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
bob <## "error receiving file 1 (test.pdf) from alice"
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO () testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO ()
testXFTPCancelRcvRepeat = testXFTPCancelRcvRepeat =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
@ -1181,7 +1273,7 @@ testXFTPCancelRcvRepeat =
bob ##> "/fr 1 ./tests/tmp" bob ##> "/fr 1 ./tests/tmp"
bob <## "saving file 1 from alice to ./tests/tmp/testfile_1" bob <## "saving file 1 from alice to ./tests/tmp/testfile_1"
-- alice <## "started sending file 1 (testfile) to bob" -- TODO "started uploading" ? -- alice <## "started sending file 1 (testfile) to bob" -- TODO "started uploading" ?
alice <## "uploaded file 1 (testfile) for bob" alice <## "completed uploading file 1 (testfile) for bob"
bob <## "started receiving file 1 (testfile) from alice" bob <## "started receiving file 1 (testfile) from alice"
bob ##> "/fc 1" bob ##> "/fc 1"