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:
parent
cbcdeb2b43
commit
6b725a8ef7
@ -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,
|
||||||
|
@ -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)}")
|
||||||
|
@ -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 = { _ -> },
|
||||||
|
@ -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 = {},
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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"),
|
||||||
|
@ -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))")
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
@ -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 =
|
||||||
|
Loading…
Reference in New Issue
Block a user