mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
core, mobile: file error statuses, cancel sent file (#2193)
This commit is contained in:
parent
6913bf1a46
commit
09481e09b6
22 changed files with 445 additions and 177 deletions
|
@ -1738,23 +1738,32 @@ class CIFile(
|
|||
is CIFileStatus.SndTransfer -> true
|
||||
is CIFileStatus.SndComplete -> true
|
||||
is CIFileStatus.SndCancelled -> true
|
||||
is CIFileStatus.SndError -> true
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvAccepted -> false
|
||||
is CIFileStatus.RcvTransfer -> false
|
||||
is CIFileStatus.RcvCancelled -> false
|
||||
is CIFileStatus.RcvComplete -> true
|
||||
is CIFileStatus.RcvError -> false
|
||||
}
|
||||
|
||||
val cancellable: Boolean = when (fileStatus) {
|
||||
is CIFileStatus.SndStored -> fileProtocol != FileProtocol.XFTP // TODO true - enable when XFTP send supports cancel
|
||||
is CIFileStatus.SndTransfer -> fileProtocol != FileProtocol.XFTP // TODO true
|
||||
is CIFileStatus.SndComplete -> false
|
||||
is CIFileStatus.SndCancelled -> false
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvAccepted -> true
|
||||
is CIFileStatus.RcvTransfer -> true
|
||||
is CIFileStatus.RcvCancelled -> false
|
||||
is CIFileStatus.RcvComplete -> false
|
||||
val cancelAction: CancelAction? = when (fileStatus) {
|
||||
is CIFileStatus.SndStored -> sndCancelAction
|
||||
is CIFileStatus.SndTransfer -> sndCancelAction
|
||||
is CIFileStatus.SndComplete ->
|
||||
if (fileProtocol == FileProtocol.XFTP) {
|
||||
revokeCancelAction
|
||||
} else {
|
||||
null
|
||||
}
|
||||
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 {
|
||||
|
@ -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
|
||||
enum class FileProtocol {
|
||||
@SerialName("smp") SMP,
|
||||
|
@ -1781,11 +1828,13 @@ 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("rcvInvitation") object RcvInvitation: CIFileStatus()
|
||||
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||
}
|
||||
|
||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
|
|
|
@ -1442,6 +1442,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||
}
|
||||
is CR.RcvFileProgressXFTP ->
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
is CR.RcvFileError -> {
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
}
|
||||
is CR.SndFileStart ->
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
is CR.SndFileComplete -> {
|
||||
|
@ -1458,6 +1462,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
}
|
||||
is CR.SndFileError -> {
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
cleanupFile(r.chatItem)
|
||||
}
|
||||
is CR.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("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("rcvFileError") class RcvFileError(val user: User, val chatItem: AChatItem): CR()
|
||||
// sending file events
|
||||
@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()
|
||||
|
@ -3139,6 +3148,8 @@ sealed class 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("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("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()
|
||||
|
@ -3238,12 +3249,14 @@ sealed class CR {
|
|||
is RcvFileCancelled -> "rcvFileCancelled"
|
||||
is RcvFileSndCancelled -> "rcvFileSndCancelled"
|
||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||
is RcvFileError -> "rcvFileError"
|
||||
is SndFileCancelled -> "sndFileCancelled"
|
||||
is SndFileComplete -> "sndFileComplete"
|
||||
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
||||
is SndFileStart -> "sndFileStart"
|
||||
is SndFileProgressXFTP -> "sndFileProgressXFTP"
|
||||
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
|
||||
is SndFileError -> "sndFileError"
|
||||
is CallInvitation -> "callInvitation"
|
||||
is CallOffer -> "callOffer"
|
||||
is CallAnswer -> "callAnswer"
|
||||
|
@ -3345,12 +3358,14 @@ sealed class CR {
|
|||
is RcvFileCancelled -> 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 RcvFileError -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileCancelled -> json.encodeToString(chatItem)
|
||||
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndFileRcvCancelled -> 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 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 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)}")
|
||||
|
|
|
@ -156,6 +156,7 @@ fun CIFileView(
|
|||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid())
|
||||
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
|
||||
|
@ -170,6 +171,7 @@ fun CIFileView(
|
|||
}
|
||||
is CIFileStatus.RcvComplete -> fileIcon()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
}
|
||||
} else {
|
||||
fileIcon()
|
||||
|
|
|
@ -82,10 +82,12 @@ fun CIImageView(
|
|||
is CIFileStatus.SndTransfer -> progressIndicator()
|
||||
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.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.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
|
@ -309,6 +310,7 @@ private fun loadingIndicator(file: CIFile?) {
|
|||
progressIndicator()
|
||||
}
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,8 +168,8 @@ fun ChatItemView(
|
|||
}
|
||||
)
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancellable) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile)
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
|
@ -278,14 +278,15 @@ fun ChatItemView(
|
|||
fun CancelFileItemAction(
|
||||
fileId: Long,
|
||||
showMenu: MutableState<Boolean>,
|
||||
cancelFile: (Long) -> Unit
|
||||
cancelFile: (Long) -> Unit,
|
||||
cancelAction: CancelAction
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.cancel_verb),
|
||||
stringResource(cancelAction.uiActionId),
|
||||
Icons.Outlined.Close,
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
cancelFileAlertDialog(fileId, cancelFile = cancelFile)
|
||||
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
|
||||
},
|
||||
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(
|
||||
title = generalGetString(R.string.cancel_file__question),
|
||||
text = generalGetString(R.string.file_transfer_will_be_cancelled_warning),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
title = generalGetString(cancelAction.alert.titleId),
|
||||
text = generalGetString(cancelAction.alert.messageId),
|
||||
confirmText = generalGetString(cancelAction.alert.confirmId),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
cancelFile(fileId)
|
||||
|
|
|
@ -218,10 +218,18 @@
|
|||
<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_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_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 -->
|
||||
<string name="icon_descr_edited">edited</string>
|
||||
|
|
|
@ -1333,6 +1333,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||
cleanupFile(aChatItem)
|
||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileError(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
|
@ -1346,6 +1349,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .sndFileError(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
cleanupFile(aChatItem)
|
||||
case let .callInvitation(invitation):
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
activateCall(invitation)
|
||||
|
|
|
@ -55,11 +55,13 @@ struct CIFileView: View {
|
|||
case .sndTransfer: return false
|
||||
case .sndComplete: return false
|
||||
case .sndCancelled: return false
|
||||
case .sndError: return false
|
||||
case .rcvInvitation: return true
|
||||
case .rcvAccepted: return true
|
||||
case .rcvTransfer: return false
|
||||
case .rcvComplete: return true
|
||||
case .rcvCancelled: return false
|
||||
case .rcvError: return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -130,6 +132,7 @@ struct CIFileView: View {
|
|||
}
|
||||
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid() {
|
||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||
|
@ -145,6 +148,7 @@ struct CIFileView: View {
|
|||
}
|
||||
case .rcvComplete: fileIcon("doc.fill")
|
||||
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
}
|
||||
} else {
|
||||
fileIcon("doc.fill")
|
||||
|
|
|
@ -93,10 +93,12 @@ struct CIImageView: View {
|
|||
case .sndTransfer: progressView()
|
||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case .rcvTransfer: progressView()
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,38 +199,33 @@ struct CIVideoView: View {
|
|||
case .xftp: progressCircle(sndProgress, sndTotal)
|
||||
case .smp: progressView()
|
||||
}
|
||||
case .sndComplete:
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 10, height: 10)
|
||||
.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 .sndComplete: fileIcon("checkmark", 10, 13)
|
||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||
case .sndError: fileIcon("xmark", 10, 13)
|
||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
|
||||
progressCircle(rcvProgress, rcvTotal)
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||
case .rcvError: fileIcon("xmark", 10, 13)
|
||||
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 {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
|
|
|
@ -108,11 +108,13 @@ struct VoiceMessagePlayer: View {
|
|||
case .sndTransfer: playbackButton()
|
||||
case .sndComplete: playbackButton()
|
||||
case .sndCancelled: playbackButton()
|
||||
case .sndError: playbackButton()
|
||||
case .rcvInvitation: loadingIcon()
|
||||
case .rcvAccepted: loadingIcon()
|
||||
case .rcvTransfer: loadingIcon()
|
||||
case .rcvComplete: playbackButton()
|
||||
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
}
|
||||
} else {
|
||||
playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||
|
|
|
@ -496,8 +496,8 @@ struct ChatView: View {
|
|||
}
|
||||
if ci.meta.itemDeleted == nil,
|
||||
let file = ci.file,
|
||||
file.cancellable {
|
||||
menu.append(cancelFileUIAction(file.fileId, sent: ci.chatDir.sent))
|
||||
let cancelAction = file.cancelAction {
|
||||
menu.append(cancelFileUIAction(file.fileId, cancelAction))
|
||||
}
|
||||
if !live || !ci.meta.isLive {
|
||||
menu.append(deleteUIAction())
|
||||
|
@ -589,15 +589,16 @@ struct ChatView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func cancelFileUIAction(_ fileId: Int64, sent: Bool) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Stop file", comment: "chat item action"),
|
||||
image: UIImage(systemName: "xmark")
|
||||
private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction {
|
||||
return UIAction(
|
||||
title: cancelAction.uiAction,
|
||||
image: UIImage(systemName: "xmark"),
|
||||
attributes: [.destructive]
|
||||
) { _ in
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text(sent ? "Stop sending file?" : "Stop receiving file?"),
|
||||
message: Text(sent ? "Sending file will be stopped." : "Receiving file will be stopped."),
|
||||
primaryButton: .destructive(Text("Stop")) {
|
||||
title: Text(cancelAction.alert.title),
|
||||
message: Text(cancelAction.alert.message),
|
||||
primaryButton: .destructive(Text(cancelAction.alert.confirm)) {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await cancelFile(user: user, fileId: fileId)
|
||||
|
|
|
@ -453,6 +453,7 @@ public enum ChatResponse: Decodable, Error {
|
|||
case rcvFileComplete(user: User, chatItem: AChatItem)
|
||||
case rcvFileCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileSndCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileError(user: User, chatItem: AChatItem)
|
||||
// sending file events
|
||||
case sndFileStart(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 sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
||||
case sndFileCompleteXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
||||
case sndFileError(user: User, chatItem: AChatItem)
|
||||
// call events
|
||||
case callInvitation(callInvitation: RcvCallInvitation)
|
||||
case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||
case callAnswer(user: User, contact: Contact, answer: WebRTCSession)
|
||||
|
@ -564,12 +567,14 @@ public enum ChatResponse: Decodable, Error {
|
|||
case .rcvFileComplete: return "rcvFileComplete"
|
||||
case .rcvFileCancelled: return "rcvFileCancelled"
|
||||
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
||||
case .rcvFileError: return "rcvFileError"
|
||||
case .sndFileStart: return "sndFileStart"
|
||||
case .sndFileComplete: return "sndFileComplete"
|
||||
case .sndFileCancelled: return "sndFileCancelled"
|
||||
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
|
||||
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
|
||||
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
|
||||
case .sndFileError: return "sndFileError"
|
||||
case .callInvitation: return "callInvitation"
|
||||
case .callOffer: return "callOffer"
|
||||
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 .rcvFileCancelled(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 .sndFileComplete(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 .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 .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
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 .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
||||
|
|
|
@ -2281,32 +2281,79 @@ public struct CIFile: Decodable {
|
|||
case .sndTransfer: return true
|
||||
case .sndComplete: return true
|
||||
case .sndCancelled: return true
|
||||
case .sndError: return true
|
||||
case .rcvInvitation: return false
|
||||
case .rcvAccepted: return false
|
||||
case .rcvTransfer: return false
|
||||
case .rcvCancelled: return false
|
||||
case .rcvComplete: return true
|
||||
case .rcvError: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var cancellable: Bool {
|
||||
public var cancelAction: CancelAction? {
|
||||
get {
|
||||
switch self.fileStatus {
|
||||
case .sndStored: return self.fileProtocol != .xftp // TODO true - enable when XFTP send supports cancel
|
||||
case .sndTransfer: return self.fileProtocol != .xftp // TODO true
|
||||
case .sndComplete: return false
|
||||
case .sndCancelled: return false
|
||||
case .rcvInvitation: return false
|
||||
case .rcvAccepted: return true
|
||||
case .rcvTransfer: return true
|
||||
case .rcvCancelled: return false
|
||||
case .rcvComplete: return false
|
||||
case .sndStored: return sndCancelAction
|
||||
case .sndTransfer: return sndCancelAction
|
||||
case .sndComplete:
|
||||
if self.fileProtocol == .xftp {
|
||||
return revokeCancelAction
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
case smp = "smp"
|
||||
case xftp = "xftp"
|
||||
|
@ -2317,11 +2364,13 @@ public enum CIFileStatus: Decodable {
|
|||
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
||||
case sndComplete
|
||||
case sndCancelled
|
||||
case sndError
|
||||
case rcvInvitation
|
||||
case rcvAccepted
|
||||
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
||||
case rcvComplete
|
||||
case rcvCancelled
|
||||
case rcvError
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
|
@ -2329,11 +2378,13 @@ public enum CIFileStatus: Decodable {
|
|||
case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
|
||||
case .sndComplete: return "sndComplete"
|
||||
case .sndCancelled: return "sndCancelled"
|
||||
case .sndError: return "sndError"
|
||||
case .rcvInvitation: return "rcvInvitation"
|
||||
case .rcvAccepted: return "rcvAccepted"
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
|
||||
case .rcvComplete: return "rcvComplete"
|
||||
case .rcvCancelled: return "rcvCancelled"
|
||||
case .rcvError: return "rcvError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1395,9 +1395,9 @@ processChatCommand = \case
|
|||
CancelFile fileId -> withUser $ \user@User {userId} ->
|
||||
withChatLock "cancelFile" . procCmd $
|
||||
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"
|
||||
| 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"
|
||||
| otherwise -> do
|
||||
fileAgentConnIds <- cancelSndFile user ftm fts True
|
||||
|
@ -1413,6 +1413,9 @@ processChatCommand = \case
|
|||
_ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
|
||||
ci <- withStore $ \db -> getChatItemByFileId db user fileId
|
||||
pure $ CRSndFileCancelled user ci ftm fts
|
||||
where
|
||||
fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} =
|
||||
s == FSCancelled || (s == FSComplete && isNothing xftpSndFile)
|
||||
FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile}
|
||||
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
|
||||
| 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
|
||||
fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId
|
||||
getSndFileTransfer db user fileId
|
||||
case msg of
|
||||
SFPROG sndProgress sndTotal ->
|
||||
unless cancelled $ do
|
||||
let status = CIFSSndTransfer {sndProgress, sndTotal}
|
||||
unless cancelled $ case msg of
|
||||
SFPROG sndProgress sndTotal -> do
|
||||
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
|
||||
liftIO $ updateCIFileStatus db user fileId status
|
||||
liftIO $ updateFileCancelled db user fileId CIFSSndError
|
||||
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
|
||||
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e
|
||||
toView $ CRSndFileError user ci
|
||||
where
|
||||
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
|
||||
fileDescrText = safeDecodeUtf8 . strEncode
|
||||
|
@ -2416,37 +2420,38 @@ processAgentMsgRcvFile _corrId aFileId msg =
|
|||
where
|
||||
process :: User -> m ()
|
||||
process user = do
|
||||
ft@RcvFileTransfer {fileId, cancelled} <- withStore $ \db -> do
|
||||
ft@RcvFileTransfer {fileId} <- withStore $ \db -> do
|
||||
fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId
|
||||
getRcvFileTransfer db user fileId
|
||||
case msg of
|
||||
RFPROG rcvProgress rcvTotal ->
|
||||
unless cancelled $ do
|
||||
let status = CIFSRcvTransfer {rcvProgress, rcvTotal}
|
||||
ci <- withStore $ \db -> do
|
||||
liftIO $ updateCIFileStatus db user fileId status
|
||||
getChatItemByFileId db user fileId
|
||||
toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
|
||||
unless (rcvFileCompleteOrCancelled ft) $ case msg of
|
||||
RFPROG rcvProgress rcvTotal -> do
|
||||
let status = CIFSRcvTransfer {rcvProgress, rcvTotal}
|
||||
ci <- withStore $ \db -> do
|
||||
liftIO $ updateCIFileStatus db user fileId status
|
||||
getChatItemByFileId db user fileId
|
||||
toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
|
||||
RFDONE xftpPath ->
|
||||
unless cancelled $ do
|
||||
case liveRcvFileTransferPath ft of
|
||||
Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
|
||||
Just targetPath -> do
|
||||
fsTargetPath <- toFSFilePath targetPath
|
||||
renameFile xftpPath fsTargetPath
|
||||
ci <- withStore $ \db -> do
|
||||
liftIO $ do
|
||||
updateRcvFileStatus db fileId FSComplete
|
||||
updateCIFileStatus db user fileId CIFSRcvComplete
|
||||
getChatItemByFileId db user fileId
|
||||
agentXFTPDeleteRcvFile user aFileId fileId
|
||||
toView $ CRRcvFileComplete user ci
|
||||
RFERR e -> do
|
||||
unless (temporaryAgentError e) $ do
|
||||
-- update chat item status
|
||||
-- send status to view
|
||||
case liveRcvFileTransferPath ft of
|
||||
Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
|
||||
Just targetPath -> do
|
||||
fsTargetPath <- toFSFilePath targetPath
|
||||
renameFile xftpPath fsTargetPath
|
||||
ci <- withStore $ \db -> do
|
||||
liftIO $ do
|
||||
updateRcvFileStatus db fileId FSComplete
|
||||
updateCIFileStatus db user fileId CIFSRcvComplete
|
||||
getChatItemByFileId db user fileId
|
||||
agentXFTPDeleteRcvFile user aFileId fileId
|
||||
toView $ CRRcvFileComplete user ci
|
||||
RFERR e
|
||||
| temporaryAgentError e ->
|
||||
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
|
||||
| otherwise -> do
|
||||
ci <- withStore $ \db -> do
|
||||
liftIO $ updateFileCancelled db user fileId CIFSRcvError
|
||||
getChatItemByFileId db user 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 user _ agentConnId END =
|
||||
|
@ -2940,9 +2945,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||
_ -> pure ()
|
||||
|
||||
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 ->
|
||||
unless cancelled $ do
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
|
||||
ci <- withStore $ \db -> getChatItemByFileId db user fileId
|
||||
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
|
||||
|
||||
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
|
||||
badRcvFileChunk ft@RcvFileTransfer {cancelled} err =
|
||||
unless cancelled $ do
|
||||
badRcvFileChunk ft err =
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
|
||||
throwChatError $ CEFileRcvChunk err
|
||||
|
||||
|
@ -3165,14 +3170,14 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||
|
||||
processFDMessage :: FileTransferId -> FileDescr -> m ()
|
||||
processFDMessage fileId fileDescr = do
|
||||
RcvFileTransfer {cancelled} <- withStore $ \db -> getRcvFileTransfer db user fileId
|
||||
unless cancelled $ do
|
||||
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
(rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do
|
||||
rfd <- appendRcvFD db userId fileId fileDescr
|
||||
-- reading second time in the same transaction as appending description
|
||||
-- to prevent race condition with accept
|
||||
ft <- getRcvFileTransfer db user fileId
|
||||
pure (rfd, ft)
|
||||
ft' <- getRcvFileTransfer db user fileId
|
||||
pure (rfd, ft')
|
||||
case fileStatus of
|
||||
RFSAccepted _ -> receiveViaCompleteFD user fileId rfd
|
||||
_ -> pure ()
|
||||
|
@ -3365,8 +3370,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||
xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
|
||||
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||
unless cancelled $ do
|
||||
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
|
||||
ci <- withStore $ \db -> getChatItemByFileId db user fileId
|
||||
toView $ CRRcvFileSndCancelled user ci ft
|
||||
|
@ -3446,8 +3451,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||
(SMDRcv, CIGroupRcv m) -> do
|
||||
if sameMemberId memberId m
|
||||
then do
|
||||
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||
unless cancelled $ do
|
||||
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
|
||||
unless (rcvFileCompleteOrCancelled ft) $ do
|
||||
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
|
||||
ci <- withStore $ \db -> getChatItemByFileId db user fileId
|
||||
toView $ CRRcvFileSndCancelled user ci ft
|
||||
|
|
|
@ -438,6 +438,7 @@ data ChatResponse
|
|||
| CRRcvFileComplete {user :: User, chatItem :: AChatItem}
|
||||
| CRRcvFileCancelled {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}
|
||||
| CRSndFileComplete {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}
|
||||
| CRSndFileCompleteXFTP {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}
|
||||
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
||||
| CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection}
|
||||
|
|
|
@ -448,11 +448,13 @@ data CIFileStatus (d :: MsgDirection) where
|
|||
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
|
||||
CIFSSndCancelled :: CIFileStatus 'MDSnd
|
||||
CIFSSndComplete :: CIFileStatus 'MDSnd
|
||||
CIFSSndError :: CIFileStatus 'MDSnd
|
||||
CIFSRcvInvitation :: CIFileStatus 'MDRcv
|
||||
CIFSRcvAccepted :: CIFileStatus 'MDRcv
|
||||
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
|
||||
CIFSRcvComplete :: CIFileStatus 'MDRcv
|
||||
CIFSRcvCancelled :: CIFileStatus 'MDRcv
|
||||
CIFSRcvError :: CIFileStatus 'MDRcv
|
||||
|
||||
deriving instance Eq (CIFileStatus d)
|
||||
|
||||
|
@ -464,11 +466,13 @@ ciFileEnded = \case
|
|||
CIFSSndTransfer {} -> False
|
||||
CIFSSndCancelled -> True
|
||||
CIFSSndComplete -> True
|
||||
CIFSSndError -> True
|
||||
CIFSRcvInvitation -> False
|
||||
CIFSRcvAccepted -> False
|
||||
CIFSRcvTransfer {} -> False
|
||||
CIFSRcvCancelled -> True
|
||||
CIFSRcvComplete -> True
|
||||
CIFSRcvError -> True
|
||||
|
||||
instance ToJSON (CIFileStatus d) where
|
||||
toJSON = J.toJSON . jsonCIFileStatus
|
||||
|
@ -488,11 +492,13 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
|
|||
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
|
||||
CIFSSndCancelled -> "snd_cancelled"
|
||||
CIFSSndComplete -> "snd_complete"
|
||||
CIFSSndError -> "snd_error"
|
||||
CIFSRcvInvitation -> "rcv_invitation"
|
||||
CIFSRcvAccepted -> "rcv_accepted"
|
||||
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
|
||||
CIFSRcvComplete -> "rcv_complete"
|
||||
CIFSRcvCancelled -> "rcv_cancelled"
|
||||
CIFSRcvError -> "rcv_error"
|
||||
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
|
||||
|
||||
instance StrEncoding ACIFileStatus where
|
||||
|
@ -503,11 +509,13 @@ instance StrEncoding ACIFileStatus where
|
|||
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
|
||||
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
|
||||
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
|
||||
"snd_error" -> pure $ AFS SMDSnd CIFSSndError
|
||||
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
|
||||
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
|
||||
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
|
||||
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
|
||||
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
|
||||
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
|
||||
_ -> fail "bad file status"
|
||||
where
|
||||
progress :: (Int64 -> Int64 -> a) -> A.Parser a
|
||||
|
@ -519,11 +527,13 @@ data JSONCIFileStatus
|
|||
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
|
||||
| JCIFSSndCancelled
|
||||
| JCIFSSndComplete
|
||||
| JCIFSSndError
|
||||
| JCIFSRcvInvitation
|
||||
| JCIFSRcvAccepted
|
||||
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
|
||||
| JCIFSRcvComplete
|
||||
| JCIFSRcvCancelled
|
||||
| JCIFSRcvError
|
||||
deriving (Generic)
|
||||
|
||||
instance ToJSON JSONCIFileStatus where
|
||||
|
@ -536,11 +546,13 @@ jsonCIFileStatus = \case
|
|||
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
|
||||
CIFSSndCancelled -> JCIFSSndCancelled
|
||||
CIFSSndComplete -> JCIFSSndComplete
|
||||
CIFSSndError -> JCIFSSndError
|
||||
CIFSRcvInvitation -> JCIFSRcvInvitation
|
||||
CIFSRcvAccepted -> JCIFSRcvAccepted
|
||||
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
|
||||
CIFSRcvComplete -> JCIFSRcvComplete
|
||||
CIFSRcvCancelled -> JCIFSRcvCancelled
|
||||
CIFSRcvError -> JCIFSRcvError
|
||||
|
||||
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
|
||||
aciFileStatusJSON = \case
|
||||
|
@ -548,11 +560,13 @@ aciFileStatusJSON = \case
|
|||
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
|
||||
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
|
||||
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
|
||||
JCIFSSndError -> AFS SMDSnd CIFSSndError
|
||||
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
|
||||
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
|
||||
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
|
||||
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
|
||||
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
|
||||
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
|
||||
|
||||
-- to conveniently read file data from db
|
||||
data CIFileInfo = CIFileInfo
|
||||
|
|
|
@ -1652,6 +1652,9 @@ rcvFileComplete = \case
|
|||
RFSComplete _ -> True
|
||||
_ -> False
|
||||
|
||||
rcvFileCompleteOrCancelled :: RcvFileTransfer -> Bool
|
||||
rcvFileCompleteOrCancelled RcvFileTransfer {fileStatus, cancelled} = rcvFileComplete fileStatus || cancelled
|
||||
|
||||
data RcvFileInfo = RcvFileInfo
|
||||
{ filePath :: FilePath,
|
||||
connId :: Maybe Int64,
|
||||
|
|
|
@ -153,12 +153,14 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
|||
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
|
||||
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
|
||||
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||
CRRcvFileError u ci -> ttyUser u $ receivingFile_' "error" ci
|
||||
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
||||
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
||||
CRSndFileStartXFTP _ _ _ -> []
|
||||
CRSndFileProgressXFTP _ _ _ _ _ -> []
|
||||
CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadedFile ci
|
||||
CRSndFileCancelledXFTP _ _ _ -> []
|
||||
CRSndFileStartXFTP {} -> []
|
||||
CRSndFileProgressXFTP {} -> []
|
||||
CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci
|
||||
CRSndFileCancelledXFTP {} -> []
|
||||
CRSndFileError u ci -> ttyUser u $ uploadingFile "error" ci
|
||||
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
||||
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
|
||||
CRContactConnecting u _ -> ttyUser u []
|
||||
|
@ -1074,12 +1076,12 @@ sendingFile_ :: StyledString -> SndFileTransfer -> [StyledString]
|
|||
sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} =
|
||||
[status <> " sending " <> sndFile ft <> " to " <> ttyContact c]
|
||||
|
||||
uploadedFile :: AChatItem -> [StyledString]
|
||||
uploadedFile (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) =
|
||||
["uploaded " <> fileTransferStr fileId fileName <> " for " <> ttyContact c]
|
||||
uploadedFile (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) =
|
||||
["uploaded " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g]
|
||||
uploadedFile _ = ["uploaded file"] -- shouldn't happen
|
||||
uploadingFile :: StyledString -> AChatItem -> [StyledString]
|
||||
uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) =
|
||||
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c]
|
||||
uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) =
|
||||
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g]
|
||||
uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen
|
||||
|
||||
sndFile :: SndFileTransfer -> StyledString
|
||||
sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName
|
||||
|
|
|
@ -343,11 +343,14 @@ xftpServerConfig =
|
|||
}
|
||||
|
||||
withXFTPServer :: IO () -> IO ()
|
||||
withXFTPServer =
|
||||
withXFTPServer = withXFTPServer' xftpServerConfig
|
||||
|
||||
withXFTPServer' :: XFTPServerConfig -> IO () -> IO ()
|
||||
withXFTPServer' cfg =
|
||||
serverBracket
|
||||
( \started -> do
|
||||
createDirectoryIfMissing False xftpServerFiles
|
||||
runXFTPServerBlocking started xftpServerConfig
|
||||
runXFTPServerBlocking started cfg
|
||||
)
|
||||
|
||||
serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO ()
|
||||
|
|
|
@ -12,6 +12,7 @@ import Simplex.Chat (roundedFDCount)
|
|||
import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig)
|
||||
import Simplex.Chat.Options (ChatOpts (..))
|
||||
import Simplex.FileTransfer.Client.Main (xftpClientCLI)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.Messaging.Util (unlessM)
|
||||
import System.Directory (copyFile, doesFileExist)
|
||||
import System.Environment (withArgs)
|
||||
|
@ -59,9 +60,12 @@ chatFileTests = do
|
|||
it "send and receive file" testXFTPFileTransfer
|
||||
it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload
|
||||
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 relative paths: send and receive file" testXFTPWithRelativePaths
|
||||
xit' "continue receiving file after restart" testXFTPContinueRcv
|
||||
it "error receiving file" testXFTPRcvError
|
||||
it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat
|
||||
|
||||
runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
|
||||
|
@ -125,9 +129,7 @@ testAcceptInlineFileSndCancelDuringTransfer =
|
|||
[ do
|
||||
alice <##. "cancelled sending file 1 (test_1MB.pdf)"
|
||||
alice <## "completed sending file 1 (test_1MB.pdf) to bob",
|
||||
do
|
||||
bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
|
||||
bob <## "alice cancelled sending file 1 (test_1MB.pdf)"
|
||||
bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
|
||||
]
|
||||
alice #> "@bob hi"
|
||||
bob <# "alice> hi"
|
||||
|
@ -988,7 +990,7 @@ testXFTPFileTransfer =
|
|||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
-- 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 <## "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 <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
-- 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
|
||||
|
||||
|
@ -1041,7 +1043,7 @@ testXFTPGroupFileTransfer =
|
|||
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
]
|
||||
-- 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
|
||||
|
@ -1065,6 +1067,71 @@ testXFTPGroupFileTransfer =
|
|||
where
|
||||
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 =
|
||||
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
||||
|
@ -1084,7 +1151,7 @@ testXFTPWithChangedConfig =
|
|||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
-- 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 <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
|
@ -1123,7 +1190,7 @@ testXFTPWithRelativePaths =
|
|||
bob ##> "/fr 1"
|
||||
bob <## "saving file 1 from alice to test.pdf"
|
||||
-- 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 <## "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 <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
-- 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
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
|
@ -1166,6 +1233,31 @@ testXFTPContinueRcv tmp = do
|
|||
where
|
||||
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 =
|
||||
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
||||
|
@ -1181,7 +1273,7 @@ testXFTPCancelRcvRepeat =
|
|||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/testfile_1"
|
||||
-- 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 ##> "/fc 1"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue