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
}
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 {
fun getSample(
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? {
val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null }
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)) {
chatModel.updateGroup(r.toGroup)
}
is CR.MemberRole ->
if (active(r.user)) {
chatModel.updateGroup(r.groupInfo)
}
is CR.RcvFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileSndCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileStart ->
@ -1419,6 +1436,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
removeFile(appContext, fileName)
}
}
is CR.SndFileRcvCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.CallInvitation -> {
@ -1870,6 +1889,7 @@ sealed class 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 ReceiveFile(val fileId: Long, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
class ShowVersion(): CC()
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 ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}"
is CancelFile -> "/fcancel $fileId"
is ShowVersion -> "/version"
}
@ -2037,6 +2058,7 @@ sealed class CC {
is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile"
is CancelFile -> "cancelFile"
is ShowVersion -> "showVersion"
}
@ -3046,13 +3068,14 @@ sealed class 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("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()
// 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()
@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("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("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()
@ -3150,12 +3173,13 @@ sealed class CR {
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
is RcvFileComplete -> "rcvFileComplete"
is RcvFileCancelled -> "rcvFileCancelled"
is RcvFileSndCancelled -> "rcvFileSndCancelled"
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is SndFileCancelled -> "sndFileCancelled"
is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled"
is SndFileStart -> "sndFileStart"
is SndGroupFileCancelled -> "sndGroupFileCancelled"
is SndFileProgressXFTP -> "sndFileProgressXFTP"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
@ -3255,12 +3279,13 @@ sealed class CR {
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> 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 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 SndGroupFileCancelled -> withUser(user, json.encodeToString(chatItem))
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 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 ->
val user = chatModel.currentUser.value
if (user != null) {
withApi { chatModel.controller.receiveFile(user, fileId) }
}
withApi { chatModel.controller.receiveFile(user, fileId) }
},
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(user, fileId) }
},
joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) }
@ -313,6 +313,7 @@ fun ChatLayout(
loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
@ -357,7 +358,7 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
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,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
@ -638,11 +640,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else {
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 {
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
@ -653,7 +655,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent) 12.dp else 76.dp,
).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 = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
@ -1119,6 +1122,7 @@ fun PreviewGroupChatLayout() {
loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },

View File

@ -43,6 +43,7 @@ fun ChatItemView(
linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> 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)) {
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
fun DeleteItemAction(
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) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question),
@ -383,6 +416,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
@ -403,6 +437,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},

View File

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

View File

@ -750,6 +750,23 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? {
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 {
let am = AlertManager.shared
switch r {
@ -1321,6 +1338,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileStart(user, aChatItem, _):
@ -1334,6 +1353,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
case let .sndFileRcvCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .callInvitation(invitation):

View File

@ -489,6 +489,11 @@ struct ChatView: View {
if revealed {
menu.append(hideUIAction())
}
if ci.meta.itemDeleted == nil,
let file = ci.file,
file.cancellable {
menu.append(cancelFileUIAction(file.fileId))
}
if !live || !ci.meta.isLive {
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 {
UIAction(
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 apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, inline: Bool?)
case cancelFile(fileId: Int64)
case showVersion
case string(String)
@ -205,6 +206,7 @@ public enum ChatCommand {
return "/freceive \(fileId) inline=\(onOff(inline))"
}
return "/freceive \(fileId)"
case let .cancelFile(fileId): return "/fcancel \(fileId)"
case .showVersion: return "/version"
case let .string(str): return str
}
@ -300,6 +302,7 @@ public enum ChatCommand {
case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile"
case .cancelFile: return "cancelFile"
case .showVersion: return "showVersion"
case .string: return "console command"
}
@ -448,12 +451,13 @@ public enum ChatResponse: Decodable, Error {
case rcvFileStart(user: User, chatItem: AChatItem)
case rcvFileProgressXFTP(user: User, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
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
case sndFileStart(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 sndGroupFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case callInvitation(callInvitation: RcvCallInvitation)
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 .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
case .rcvFileComplete: return "rcvFileComplete"
case .rcvFileCancelled: return "rcvFileCancelled"
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled"
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndGroupFileCancelled: return "sndGroupFileCancelled"
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
case .callInvitation: return "callInvitation"
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 .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 .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 .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 .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 .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))")

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 {

View File

@ -1383,7 +1383,9 @@ processChatCommand = \case
withChatLock "cancelFile" . procCmd $
withStore (\db -> getFileTransfer db user fileId) >>= \case
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
fileAgentConnIds <- cancelSndFile user ftm fts True
deleteAgentConnectionsAsync user fileAgentConnIds
@ -1398,8 +1400,9 @@ processChatCommand = \case
_ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
ci <- withStore $ \db -> getChatItemByFileId db user fileId
pure $ CRSndFileCancelled user ci ftm fts
FTRcv ftr@RcvFileTransfer {cancelled}
| cancelled -> throwChatError $ CEFileAlreadyCancelled fileId
FTRcv ftr@RcvFileTransfer {cancelled, fileStatus}
| cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled"
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do
cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user)
ci <- withStore $ \db -> getChatItemByFileId db user fileId
@ -2281,48 +2284,50 @@ processAgentMsgSndFile _corrId aFileId msg =
where
process :: User -> m ()
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
SFPROG sndProgress sndTotal -> do
let status = CIFSSndTransfer {sndProgress, sndTotal}
(ci, ft) <- withStore $ \db -> do
liftIO $ updateCIFileStatus db user fileId status
ft <- getFileTransferMeta db user fileId
(,ft) <$> getChatItemByFileId db user fileId
toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE _sndDescr rfds -> do
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
(ft, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId
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 : _, sft : _, SMDSnd, DirectChat ct) -> do
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g
forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
-- TODO update database status and send event to view CRSndFileCompleteXFTP
pure ()
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?
SFPROG sndProgress sndTotal ->
unless cancelled $ 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 ->
unless cancelled $ do
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 : _, sft : _, SMDSnd, DirectChat ct) -> do
msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g
forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
-- TODO update database status and send event to view CRSndFileCompleteXFTP
pure ()
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?
where
sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
sendFileDescription sft rfd msgId sendMsg = do
@ -2348,28 +2353,31 @@ processAgentMsgRcvFile _corrId aFileId msg =
where
process :: User -> m ()
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
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 -> do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
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
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
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
-- update chat item status
-- send status to view
@ -2757,7 +2765,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user)
case err of
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
_ -> throwChatError $ CEFileSend fileId err
MSG meta _ _ -> do

View File

@ -770,7 +770,7 @@ data ChatErrorType
| CEFileNotFound {message :: String}
| CEFileAlreadyReceiving {message :: String}
| CEFileCancelled {message :: String}
| CEFileAlreadyCancelled {fileId :: FileTransferId}
| CEFileCancel {fileId :: FileTransferId, message :: String}
| CEFileAlreadyExists {filePath :: FilePath}
| CEFileRead {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"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RFS"
rcvFileComplete :: RcvFileStatus -> Bool
rcvFileComplete = \case
RFSComplete _ -> True
_ -> False
data RcvFileInfo = RcvFileInfo
{ filePath :: FilePath,
connId :: Maybe Int64,

View File

@ -1274,7 +1274,7 @@ viewChatError logLevel = \case
CEFileNotFound f -> ["file not found: " <> plain f]
CEFileAlreadyReceiving f -> ["file is already being received: " <> 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]
CEFileRead f e -> ["cannot read 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 ##> "/fs 1"
alice <## "sending file 1 (test.jpg) cancelled: bob"
alice <## "file transfer cancelled"
]
checkPartialTransfer "test.jpg"
@ -606,6 +607,7 @@ testFilesFoldersImageRcvDelete =
alice <## "bob cancelled receiving file 1 (test.jpg)"
alice ##> "/fs 1"
alice <## "sending file 1 (test.jpg) cancelled: bob"
alice <## "file transfer cancelled"
testSendImageWithTextAndQuote :: HasCallStack => FilePath -> IO ()
testSendImageWithTextAndQuote =