ios, android: cancel file UI; core: cancel file fixes (#2100)

backend fixes:
- check file is not complete on CancelFile,
- check file is not cancelled when processing XFTP events,
- mark SMP file cancelled if recipient cancelled in direct chat.
This commit is contained in:
spaced4ndy 2023-03-30 14:10:13 +04:00 committed by GitHub
parent cbcdeb2b43
commit 6b725a8ef7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 255 additions and 89 deletions

View File

@ -1726,6 +1726,18 @@ class CIFile(
is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvComplete -> true
} }
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
}
companion object { companion object {
fun getSample( fun getSample(
fileId: Long = 1, fileId: Long = 1,

View File

@ -998,6 +998,25 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
} }
} }
suspend fun cancelFile(user: User, fileId: Long) {
val chatItem = apiCancelFile(fileId)
if (chatItem != null) {
chatItemSimpleUpdate(user, chatItem)
}
}
suspend fun apiCancelFile(fileId: Long): AChatItem? {
val r = sendCmd(CC.CancelFile(fileId))
return when (r) {
is CR.SndFileCancelled -> r.chatItem
is CR.RcvFileCancelled -> r.chatItem
else -> {
Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}")
null
}
}
}
suspend fun apiNewGroup(p: GroupProfile): GroupInfo? { suspend fun apiNewGroup(p: GroupProfile): GroupInfo? {
val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null } val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null }
val r = sendCmd(CC.ApiNewGroup(userId, p)) val r = sendCmd(CC.ApiNewGroup(userId, p))
@ -1394,14 +1413,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (active(r.user)) { if (active(r.user)) {
chatModel.updateGroup(r.toGroup) chatModel.updateGroup(r.toGroup)
} }
is CR.MemberRole ->
if (active(r.user)) {
chatModel.updateGroup(r.groupInfo)
}
is CR.RcvFileStart -> is CR.RcvFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete -> is CR.RcvFileComplete ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileSndCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileProgressXFTP -> is CR.RcvFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileStart -> is CR.SndFileStart ->
@ -1419,6 +1436,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
removeFile(appContext, fileName) removeFile(appContext, fileName)
} }
} }
is CR.SndFileRcvCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileProgressXFTP -> is CR.SndFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.CallInvitation -> { is CR.CallInvitation -> {
@ -1870,6 +1889,7 @@ sealed class CC {
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val inline: Boolean?): CC() class ReceiveFile(val fileId: Long, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
class ShowVersion(): CC() class ShowVersion(): CC()
val cmdString: String get() = when (this) { val cmdString: String get() = when (this) {
@ -1953,6 +1973,7 @@ sealed class CC {
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}" is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}"
is CancelFile -> "/fcancel $fileId"
is ShowVersion -> "/version" is ShowVersion -> "/version"
} }
@ -2037,6 +2058,7 @@ sealed class CC {
is ApiChatRead -> "apiChatRead" is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread" is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile" is ReceiveFile -> "receiveFile"
is CancelFile -> "cancelFile"
is ShowVersion -> "showVersion" is ShowVersion -> "showVersion"
} }
@ -3046,13 +3068,14 @@ sealed class CR {
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): 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("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): 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()
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): 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("sndGroupFileCancelled") class SndGroupFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<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("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()
@ -3150,12 +3173,13 @@ sealed class CR {
is RcvFileAccepted -> "rcvFileAccepted" is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart" is RcvFileStart -> "rcvFileStart"
is RcvFileComplete -> "rcvFileComplete" is RcvFileComplete -> "rcvFileComplete"
is RcvFileCancelled -> "rcvFileCancelled"
is RcvFileSndCancelled -> "rcvFileSndCancelled"
is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
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 SndGroupFileCancelled -> "sndGroupFileCancelled"
is SndFileProgressXFTP -> "sndFileProgressXFTP" is SndFileProgressXFTP -> "sndFileProgressXFTP"
is CallInvitation -> "callInvitation" is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer" is CallOffer -> "callOffer"
@ -3255,12 +3279,13 @@ sealed class CR {
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> withUser(user, json.encodeToString(chatItem)) is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem)) is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
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 RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
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 SndGroupFileCancelled -> 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 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)}")

View File

@ -230,10 +230,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
} }
}, },
receiveFile = { fileId -> receiveFile = { fileId ->
val user = chatModel.currentUser.value withApi { chatModel.controller.receiveFile(user, fileId) }
if (user != null) { },
withApi { chatModel.controller.receiveFile(user, fileId) } cancelFile = { fileId ->
} withApi { chatModel.controller.cancelFile(user, fileId) }
}, },
joinGroup = { groupId -> joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) } withApi { chatModel.controller.apiJoinGroup(groupId) }
@ -313,6 +313,7 @@ fun ChatLayout(
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit, startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
@ -357,7 +358,7 @@ fun ChatLayout(
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue, chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage, useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
) )
} }
} }
@ -530,6 +531,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
@ -638,11 +640,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else { } else {
Spacer(Modifier.size(42.dp)) Spacer(Modifier.size(42.dp))
} }
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} else { } else {
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) { Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} }
} else { // direct message } else { // direct message
@ -653,7 +655,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent) 12.dp else 76.dp, end = if (sent) 12.dp else 76.dp,
).then(swipeableModifier) ).then(swipeableModifier)
) { ) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem) ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
} }
} }
@ -1060,6 +1062,7 @@ fun PreviewChatLayout() {
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
@ -1119,6 +1122,7 @@ fun PreviewGroupChatLayout() {
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },

View File

@ -43,6 +43,7 @@ fun ChatItemView(
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit, scrollToItem: (Long) -> Unit,
@ -168,6 +169,9 @@ fun ChatItemView(
} }
) )
} }
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancellable) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile)
}
if (!(live && cItem.meta.isLive)) { if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
} }
@ -270,6 +274,23 @@ fun ChatItemView(
} }
} }
@Composable
fun CancelFileItemAction(
fileId: Long,
showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit
) {
ItemAction(
stringResource(R.string.cancel_verb),
Icons.Outlined.Close,
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile)
},
color = Color.Red
)
}
@Composable @Composable
fun DeleteItemAction( fun DeleteItemAction(
cItem: ChatItem, cItem: ChatItem,
@ -323,6 +344,18 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
} }
} }
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit) {
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),
destructive = true,
onConfirm = {
cancelFile(fileId)
}
)
}
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons( AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question), title = generalGetString(R.string.delete_message__question),
@ -383,6 +416,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},
@ -403,6 +437,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
cancelFile = {},
joinGroup = {}, joinGroup = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},

View File

@ -191,6 +191,8 @@
<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>

View File

@ -750,6 +750,23 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? {
return nil return nil
} }
func cancelFile(user: User, fileId: Int64) async {
if let chatItem = await apiCancelFile(fileId: fileId) {
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
}
}
func apiCancelFile(fileId: Int64) async -> AChatItem? {
let r = await chatSendCmd(.cancelFile(fileId: fileId))
switch r {
case let .sndFileCancelled(_, chatItem, _, _) : return chatItem
case let .rcvFileCancelled(_, chatItem, _) : return chatItem
default:
logger.error("apiCancelFile error: \(String(describing: r))")
return nil
}
}
func networkErrorAlert(_ r: ChatResponse) -> Bool { func networkErrorAlert(_ r: ChatResponse) -> Bool {
let am = AlertManager.shared let am = AlertManager.shared
switch r { switch r {
@ -1321,6 +1338,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem): case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileProgressXFTP(user, aChatItem, _, _): case let .rcvFileProgressXFTP(user, aChatItem, _, _):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .sndFileStart(user, aChatItem, _): case let .sndFileStart(user, aChatItem, _):
@ -1334,6 +1353,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
let fileName = cItem.file?.filePath { let fileName = cItem.file?.filePath {
removeFile(fileName) removeFile(fileName)
} }
case let .sndFileRcvCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileProgressXFTP(user, aChatItem, _, _, _): case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .callInvitation(invitation): case let .callInvitation(invitation):

View File

@ -489,6 +489,11 @@ struct ChatView: View {
if revealed { if revealed {
menu.append(hideUIAction()) menu.append(hideUIAction())
} }
if ci.meta.itemDeleted == nil,
let file = ci.file,
file.cancellable {
menu.append(cancelFileUIAction(file.fileId))
}
if !live || !ci.meta.isLive { if !live || !ci.meta.isLive {
menu.append(deleteUIAction()) menu.append(deleteUIAction())
} }
@ -579,6 +584,27 @@ struct ChatView: View {
} }
} }
private func cancelFileUIAction(_ fileId: Int64) -> UIAction {
UIAction(
title: NSLocalizedString("Cancel", comment: "chat item action"),
image: UIImage(systemName: "xmark"),
attributes: [.destructive]
) { _ in
AlertManager.shared.showAlert(Alert(
title: Text("Cancel file transfer?"),
message: Text("File transfer will be cancelled. If it's in progress it will be stoppped."),
primaryButton: .destructive(Text("Confirm")) {
Task {
if let user = ChatModel.shared.currentUser {
await cancelFile(user: user, fileId: fileId)
}
}
},
secondaryButton: .cancel()
))
}
}
private func hideUIAction() -> UIAction { private func hideUIAction() -> UIAction {
UIAction( UIAction(
title: NSLocalizedString("Hide", comment: "chat item action"), title: NSLocalizedString("Hide", comment: "chat item action"),

View File

@ -100,6 +100,7 @@ public enum ChatCommand {
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, inline: Bool?) case receiveFile(fileId: Int64, inline: Bool?)
case cancelFile(fileId: Int64)
case showVersion case showVersion
case string(String) case string(String)
@ -205,6 +206,7 @@ public enum ChatCommand {
return "/freceive \(fileId) inline=\(onOff(inline))" return "/freceive \(fileId) inline=\(onOff(inline))"
} }
return "/freceive \(fileId)" return "/freceive \(fileId)"
case let .cancelFile(fileId): return "/fcancel \(fileId)"
case .showVersion: return "/version" case .showVersion: return "/version"
case let .string(str): return str case let .string(str): return str
} }
@ -300,6 +302,7 @@ public enum ChatCommand {
case .apiChatRead: return "apiChatRead" case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread" case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile" case .receiveFile: return "receiveFile"
case .cancelFile: return "cancelFile"
case .showVersion: return "showVersion" case .showVersion: return "showVersion"
case .string: return "console command" case .string: return "console command"
} }
@ -448,12 +451,13 @@ public enum ChatResponse: Decodable, Error {
case rcvFileStart(user: User, chatItem: AChatItem) case rcvFileStart(user: User, chatItem: AChatItem)
case rcvFileProgressXFTP(user: User, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64) case rcvFileProgressXFTP(user: User, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
case rcvFileComplete(user: User, chatItem: AChatItem) case rcvFileComplete(user: User, chatItem: AChatItem)
case rcvFileCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
case rcvFileSndCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
// 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)
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [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 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)
@ -557,11 +561,12 @@ public enum ChatResponse: Decodable, Error {
case .rcvFileStart: return "rcvFileStart" case .rcvFileStart: return "rcvFileStart"
case .rcvFileProgressXFTP: return "rcvFileProgressXFTP" case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
case .rcvFileComplete: return "rcvFileComplete" case .rcvFileComplete: return "rcvFileComplete"
case .rcvFileCancelled: return "rcvFileCancelled"
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
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 .sndGroupFileCancelled: return "sndGroupFileCancelled"
case .sndFileProgressXFTP: return "sndFileProgressXFTP" case .sndFileProgressXFTP: return "sndFileProgressXFTP"
case .callInvitation: return "callInvitation" case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer" case .callOffer: return "callOffer"
@ -668,11 +673,12 @@ public enum ChatResponse: Decodable, Error {
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem)) case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)") case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
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 .rcvFileSndCancelled(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(chatItem, _): return 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 .sndGroupFileCancelled(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 .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))")

View File

@ -2236,6 +2236,22 @@ public struct CIFile: Decodable {
} }
} }
} }
public var cancellable: Bool {
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
}
}
}
} }
public enum FileProtocol: String, Decodable { public enum FileProtocol: String, Decodable {

View File

@ -1383,7 +1383,9 @@ processChatCommand = \case
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 {cancelled} fts
| cancelled -> throwChatError $ CEFileAlreadyCancelled fileId | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| not (null fts) && all (\SndFileTransfer {fileStatus = s} -> s == FSComplete || s == FSCancelled) fts ->
throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do | otherwise -> do
fileAgentConnIds <- cancelSndFile user ftm fts True fileAgentConnIds <- cancelSndFile user ftm fts True
deleteAgentConnectionsAsync user fileAgentConnIds deleteAgentConnectionsAsync user fileAgentConnIds
@ -1398,8 +1400,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
FTRcv ftr@RcvFileTransfer {cancelled} FTRcv ftr@RcvFileTransfer {cancelled, fileStatus}
| cancelled -> throwChatError $ CEFileAlreadyCancelled fileId | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do | otherwise -> do
cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user)
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> getChatItemByFileId db user fileId
@ -2281,48 +2284,50 @@ processAgentMsgSndFile _corrId aFileId msg =
where where
process :: User -> m () process :: User -> m ()
process user = do process user = do
fileId <- withStore $ \db -> getXFTPSndFileDBId db user $ AgentSndFileId aFileId (ft@FileTransferMeta {fileId, cancelled}, sfts) <- withStore $ \db -> do
fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId
getSndFileTransfer db user fileId
case msg of case msg of
SFPROG sndProgress sndTotal -> do SFPROG sndProgress sndTotal ->
let status = CIFSSndTransfer {sndProgress, sndTotal} unless cancelled $ do
(ci, ft) <- withStore $ \db -> do let status = CIFSSndTransfer {sndProgress, sndTotal}
liftIO $ updateCIFileStatus db user fileId status ci <- withStore $ \db -> do
ft <- getFileTransferMeta db user fileId liftIO $ updateCIFileStatus db user fileId status
(,ft) <$> getChatItemByFileId db user fileId getChatItemByFileId db user fileId
toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE _sndDescr rfds -> do SFDONE _sndDescr rfds ->
ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <- unless cancelled $ do
withStore $ \db -> getChatItemByFileId db user fileId ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <-
case (msgId_, itemDeleted) of withStore $ \db -> getChatItemByFileId db user fileId
(Just sharedMsgId, Nothing) -> do case (msgId_, itemDeleted) of
(ft, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId (Just sharedMsgId, Nothing) -> do
when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send" when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
-- TODO either update database status or move to SFPROG -- TODO either update database status or move to SFPROG
toView $ CRSndFileProgressXFTP user ci ft 1 1 toView $ CRSndFileProgressXFTP user ci ft 1 1
case (rfds, sfts, d, cInfo) of case (rfds, sfts, d, cInfo) of
(rfd : _, sft : _, SMDSnd, DirectChat ct) -> do (rfd : _, sft : _, SMDSnd, DirectChat ct) -> do
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g ms <- withStore' $ \db -> getGroupMembers db user g
forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user)) forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
-- TODO update database status and send event to view CRSndFileCompleteXFTP -- TODO update database status and send event to view CRSndFileCompleteXFTP
pure () pure ()
where where
memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)]
memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts')
where where
mConns' = mapMaybe useMember ms mConns' = mapMaybe useMember ms
sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts
useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}}
| (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn) | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn)
| otherwise = Nothing | otherwise = Nothing
useMember _ = Nothing useMember _ = Nothing
sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m () sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m ()
sendToMember (rfd, (conn, sft)) = sendToMember (rfd, (conn, sft)) =
void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
_ -> pure () _ -> pure ()
_ -> pure () -- TODO error? _ -> pure () -- TODO error?
where where
sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64 sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
sendFileDescription sft rfd msgId sendMsg = do sendFileDescription sft rfd msgId sendMsg = do
@ -2348,28 +2353,31 @@ processAgentMsgRcvFile _corrId aFileId msg =
where where
process :: User -> m () process :: User -> m ()
process user = do process user = do
fileId <- withStore (`getXFTPRcvFileDBId` AgentRcvFileId aFileId) ft@RcvFileTransfer {fileId, cancelled} <- withStore $ \db -> do
fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId
getRcvFileTransfer db user fileId
case msg of case msg of
RFPROG rcvProgress rcvTotal -> do RFPROG rcvProgress rcvTotal ->
let status = CIFSRcvTransfer {rcvProgress, rcvTotal} unless cancelled $ do
ci <- withStore $ \db -> do let status = CIFSRcvTransfer {rcvProgress, rcvTotal}
liftIO $ updateCIFileStatus db user fileId status ci <- withStore $ \db -> do
getChatItemByFileId db user fileId liftIO $ updateCIFileStatus db user fileId status
toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal getChatItemByFileId db user fileId
RFDONE xftpPath -> do toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
ft <- withStore $ \db -> getRcvFileTransfer db user fileId RFDONE xftpPath ->
case liveRcvFileTransferPath ft of unless cancelled $ do
Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file" case liveRcvFileTransferPath ft of
Just targetPath -> do Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
fsTargetPath <- toFSFilePath targetPath Just targetPath -> do
renameFile xftpPath fsTargetPath fsTargetPath <- toFSFilePath targetPath
ci <- withStore $ \db -> do renameFile xftpPath fsTargetPath
liftIO $ do ci <- withStore $ \db -> do
updateRcvFileStatus db fileId FSComplete liftIO $ do
updateCIFileStatus db user fileId CIFSRcvComplete updateRcvFileStatus db fileId FSComplete
getChatItemByFileId db user fileId updateCIFileStatus db user fileId CIFSRcvComplete
agentXFTPDeleteRcvFile user aFileId fileId getChatItemByFileId db user fileId
toView $ CRRcvFileComplete user ci agentXFTPDeleteRcvFile user aFileId fileId
toView $ CRRcvFileComplete user ci
RFERR _e -> do RFERR _e -> do
-- update chat item status -- update chat item status
-- send status to view -- send status to view
@ -2757,7 +2765,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user)
case err of case err of
SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do
ci <- withStore $ \db -> getChatItemByFileId db user fileId ci <- withStore $ \db -> do
getChatRefByFileId db user fileId >>= \case
ChatRef CTDirect _ -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled
_ -> pure ()
getChatItemByFileId db user fileId
toView $ CRSndFileRcvCancelled user ci ft toView $ CRSndFileRcvCancelled user ci ft
_ -> throwChatError $ CEFileSend fileId err _ -> throwChatError $ CEFileSend fileId err
MSG meta _ _ -> do MSG meta _ _ -> do

View File

@ -770,7 +770,7 @@ data ChatErrorType
| CEFileNotFound {message :: String} | CEFileNotFound {message :: String}
| CEFileAlreadyReceiving {message :: String} | CEFileAlreadyReceiving {message :: String}
| CEFileCancelled {message :: String} | CEFileCancelled {message :: String}
| CEFileAlreadyCancelled {fileId :: FileTransferId} | CEFileCancel {fileId :: FileTransferId, message :: String}
| CEFileAlreadyExists {filePath :: FilePath} | CEFileAlreadyExists {filePath :: FilePath}
| CEFileRead {filePath :: FilePath, message :: String} | CEFileRead {filePath :: FilePath, message :: String}
| CEFileWrite {filePath :: FilePath, message :: String} | CEFileWrite {filePath :: FilePath, message :: String}

View File

@ -1614,6 +1614,11 @@ instance ToJSON RcvFileStatus where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RFS" toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RFS"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RFS" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RFS"
rcvFileComplete :: RcvFileStatus -> Bool
rcvFileComplete = \case
RFSComplete _ -> True
_ -> False
data RcvFileInfo = RcvFileInfo data RcvFileInfo = RcvFileInfo
{ filePath :: FilePath, { filePath :: FilePath,
connId :: Maybe Int64, connId :: Maybe Int64,

View File

@ -1274,7 +1274,7 @@ viewChatError logLevel = \case
CEFileNotFound f -> ["file not found: " <> plain f] CEFileNotFound f -> ["file not found: " <> plain f]
CEFileAlreadyReceiving f -> ["file is already being received: " <> plain f] CEFileAlreadyReceiving f -> ["file is already being received: " <> plain f]
CEFileCancelled f -> ["file cancelled: " <> plain f] CEFileCancelled f -> ["file cancelled: " <> plain f]
CEFileAlreadyCancelled fileId -> ["file already cancelled: " <> sShow fileId] CEFileCancel fileId e -> ["error cancelling file " <> sShow fileId <> ": " <> sShow e]
CEFileAlreadyExists f -> ["file already exists: " <> plain f] CEFileAlreadyExists f -> ["file already exists: " <> plain f]
CEFileRead f e -> ["cannot read file " <> plain f, sShow e] CEFileRead f e -> ["cannot read file " <> plain f, sShow e]
CEFileWrite f e -> ["cannot write file " <> plain f, sShow e] CEFileWrite f e -> ["cannot write file " <> plain f, sShow e]

View File

@ -275,6 +275,7 @@ testFileRcvCancel =
alice <## "bob cancelled receiving file 1 (test.jpg)" alice <## "bob cancelled receiving file 1 (test.jpg)"
alice ##> "/fs 1" alice ##> "/fs 1"
alice <## "sending file 1 (test.jpg) cancelled: bob" alice <## "sending file 1 (test.jpg) cancelled: bob"
alice <## "file transfer cancelled"
] ]
checkPartialTransfer "test.jpg" checkPartialTransfer "test.jpg"
@ -606,6 +607,7 @@ testFilesFoldersImageRcvDelete =
alice <## "bob cancelled receiving file 1 (test.jpg)" alice <## "bob cancelled receiving file 1 (test.jpg)"
alice ##> "/fs 1" alice ##> "/fs 1"
alice <## "sending file 1 (test.jpg) cancelled: bob" alice <## "sending file 1 (test.jpg) cancelled: bob"
alice <## "file transfer cancelled"
testSendImageWithTextAndQuote :: HasCallStack => FilePath -> IO () testSendImageWithTextAndQuote :: HasCallStack => FilePath -> IO ()
testSendImageWithTextAndQuote = testSendImageWithTextAndQuote =