diff --git a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift index 6cd723a9a562acf26f481dcb72fb489c472408e2..1fad9f333906f4f0bac76f3a5cc947c91ffd2aba 100644 --- a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift +++ b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift @@ -2,6 +2,7 @@ * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -165,6 +166,19 @@ final class InteractionDataHelper { } } + func delete (interactionId: Int64, dataBase: Connection) -> Bool { + let query = table.filter(id == interactionId) + do { + let deletedRows = try dataBase.run(query.delete()) + guard deletedRows == 1 else { + return false + } + return true + } catch _ { + return false + } + } + func selectInteraction (interactionId: Int64, dataBase: Connection) throws -> Interaction? { let query = table.filter(id == interactionId) let items = try dataBase.prepare(query) diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift index ad937d1007f21a1c7bde114ffbd47270c95d75e2..200473c77a0196bd9db76595214751132e6dbd55 100644 --- a/Ring/Ring/Database/DBManager.swift +++ b/Ring/Ring/Database/DBManager.swift @@ -2,6 +2,7 @@ * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -142,6 +143,7 @@ enum DBBridgingError: Error { case updateIntercationFailed case deleteConversationFailed case getProfileFailed + case deleteMessageFailed } enum InteractionType: String { @@ -393,6 +395,21 @@ class DBManager { } } + func deleteMessage(messagesId: Int64, accountId: String) -> Completable { + return Completable.create { [unowned self] completable in + if let dataBase = self.dbConnections.forAccount(account: accountId) { + if self.interactionHepler.delete(interactionId: messagesId, dataBase: dataBase) { + completable(.completed) + } else { + completable(.error(DBBridgingError.deleteMessageFailed)) + } + } else { + completable(.error(DBBridgingError.deleteMessageFailed)) + } + return Disposables.create { } + } + } + func clearAllHistoryFor(accountId: String) -> Completable { return Completable.create { [unowned self] completable in do { diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift index 1ac05c7526afd900874e581c21b6a7f1b72b74b2..b464d3da8a85b57eb76842d2782a4f34e1fa886b 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift @@ -25,12 +25,15 @@ import UIKit import Reusable import RxSwift +import RxCocoa import ActiveLabel import SwiftyBeaver // swiftlint:disable type_body_length class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { + // MARK: Properties + let log = SwiftyBeaver.self @IBOutlet weak var avatarView: UIView! @@ -69,7 +72,19 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { var playerHeight = Variable<CGFloat>(0) + private(set) var messageId: Int64? + private var isCopyable: Bool = false + private let _deleteMessage = BehaviorRelay<Bool>(value: false) + var deleteMessage: Observable<Bool> { _deleteMessage.asObservable() } + + private var longGestureRecognizer: UILongPressGestureRecognizer? + + // MARK: prepareForReuse + override func prepareForReuse() { + self.prepareForReuseLongGesture() + self.setCellTimeLabelVisibility(hide: true) + if self.sendingIndicator != nil { self.sendingIndicator.stopAnimating() } @@ -78,11 +93,21 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.transferProgressView.removeFromSuperview() self.playerView?.removeFromSuperview() self.composingMsg.removeFromSuperview() - playerHeight.value = 0 + self.playerHeight.value = 0 self.disposeBag = DisposeBag() super.prepareForReuse() } + private func prepareForReuseLongGesture() { + self.messageId = nil + self.isCopyable = false + self._deleteMessage.accept(false) + if let longGestureRecognizer = longGestureRecognizer { + self.bubble.removeGestureRecognizer(longGestureRecognizer) + self.longGestureRecognizer = nil + } + } + func startProgressMonitor(_ item: MessageViewModel, _ conversationViewModel: ConversationViewModel) { if self.outgoingImageProgressUpdater != nil { @@ -145,7 +170,17 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { } } - func showCopyMenu() { + private func configureLongGesture(_ messageId: Int64, _ bubblePosition: BubblePosition, _ isTransfer: Bool) { + self.messageId = messageId + self.isCopyable = bubblePosition != .generated && !isTransfer + + self.bubble.isUserInteractionEnabled = true + longGestureRecognizer = UILongPressGestureRecognizer() + longGestureRecognizer!.rx.event.bind(onNext: { [weak self] _ in self?.showCopyMenu() }).disposed(by: self.disposeBag) + self.bubble.addGestureRecognizer(longGestureRecognizer!) + } + + private func showCopyMenu() { becomeFirstResponder() let menu = UIMenuController.shared if !menu.isMenuVisible { @@ -154,40 +189,35 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { } } - func setup() { - let longGestureRecognizer = UILongPressGestureRecognizer() - self.messageLabel.isUserInteractionEnabled = true - self.messageLabel.addGestureRecognizer(longGestureRecognizer) - longGestureRecognizer.rx.event.bind(onNext: { [weak self] _ in - self?.showCopyMenu() - }).disposed(by: self.disposeBag) - } - override func copy(_ sender: Any?) { UIPasteboard.general.string = self.messageLabel.text UIMenuController.shared.setMenuVisible(false, animated: true) } + override func delete(_ sender: Any?) { + _deleteMessage.accept(true) + } + override var canBecomeFirstResponder: Bool { return true } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if action == #selector(UIResponderStandardEditActions.copy) { - return true - } - return false + return action == #selector(UIResponderStandardEditActions.copy) && self.isCopyable || + action == #selector(UIResponderStandardEditActions.delete) + } + + private func setCellTimeLabelVisibility(hide: Bool) { + self.timeLabel.isHidden = hide + self.leftDivider.isHidden = hide + self.rightDivider.isHidden = hide } - func formatCellTimeLabel(_ item: MessageViewModel) { + private func formatCellTimeLabel(_ item: MessageViewModel) { // hide for potentially reused cell - self.timeLabel.isHidden = true - self.leftDivider.isHidden = true - self.rightDivider.isHidden = true + self.setCellTimeLabelVisibility(hide: true) - if item.timeStringShown == nil { - return - } + if item.timeStringShown == nil { return } // setup the label self.timeLabel.text = item.timeStringShown @@ -195,32 +225,26 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.timeLabel.font = UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.medium) // show the time - self.timeLabel.isHidden = false - self.leftDivider.isHidden = false - self.rightDivider.isHidden = false + self.setCellTimeLabelVisibility(hide: false) } // swiftlint:disable cyclomatic_complexity func applyBubbleStyleToCell(_ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) { - guard let items = items else { - return - } + + guard let items = items else { return } let item = items[indexPath.row] - let type = item.bubblePosition() - var bubbleColor: UIColor - if item.isTransfer { - if item.content.containsOnlyEmoji { - bubbleColor = UIColor.jamiMsgCellEmoji - } else { - bubbleColor = type == .received ? UIColor.jamiMsgCellReceived : UIColor(hex: 0xcfebf5, alpha: 1.0) - } - } else { + + let bubbleColor: UIColor = { (bubblePosition: BubblePosition) -> UIColor in if item.content.containsOnlyEmoji { - bubbleColor = UIColor.jamiMsgCellEmoji + return UIColor.jamiMsgCellEmoji + } else if bubblePosition == .received { + return UIColor.jamiMsgCellReceived + } else if item.isTransfer { + return UIColor(hex: 0xcfebf5, alpha: 1.0) } else { - bubbleColor = type == .received ? UIColor.jamiMsgCellReceived : UIColor.jamiMsgCellSent + return UIColor.jamiMsgCellSent } - } + }(item.bubblePosition()) if item.isTransfer { self.messageLabel.enabledTypes = [] @@ -233,7 +257,6 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { } } else { self.messageLabel.enabledTypes = [.url] - self.setup() self.messageLabel.setTextWithLineSpacing(withText: item.content, withLineSpacing: 2) self.messageLabel.handleURLTap { url in let urlString = url.absoluteString @@ -303,18 +326,17 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.bubbleViewMask?.backgroundColor = UIColor.jamiMsgBackground self.transferImageView.backgroundColor = UIColor.jamiMsgBackground buttonsHeightConstraint?.priority = UILayoutPriority(rawValue: 999.0) - guard let item = items?[indexPath.row] else { - return - } + guard let item = items?[indexPath.row] else { return } self.transferImageView.removeFromSuperview() self.playerView?.removeFromSuperview() self.composingMsg.removeFromSuperview() - playerHeight.value = 0 + self.playerHeight.value = 0 self.bubbleViewMask?.isHidden = true // hide/show time label self.formatCellTimeLabel(item) + self.configureLongGesture(item.message.messageId, item.bubblePosition(), item.isTransfer) if item.bubblePosition() == .generated { self.bubble.backgroundColor = UIColor.jamiMsgCellReceived @@ -412,19 +434,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { .bind(to: self.failedStatusLabel.rx.isHidden) .disposed(by: self.disposeBag) if self.messageReadIndicator != nil { - Observable<(Data?, String, Bool)>.combineLatest(conversationViewModel.profileImageData.asObservable(), - conversationViewModel.bestName.asObservable(), - item.displayReadIndicator.asObservable()) { ($0, $1, $2) } - .observeOn(MainScheduler.instance) - .startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value, item.displayReadIndicator.value)) - .subscribe({ [weak self] profileData -> Void in - guard let bestName = profileData.element?.1 else { return } - self?.messageReadIndicator?.subviews.forEach({ $0.removeFromSuperview() }) - if let displayReadIndicator = profileData.element?.2, displayReadIndicator { - self?.messageReadIndicator?.addSubview(AvatarView(profileImageData: profileData.element?.0, username: bestName, size: 12)) - } - }) - .disposed(by: self.disposeBag) + configureMessageReadAvatar(item, conversationViewModel) } } } else if item.bubblePosition() == .received { @@ -443,33 +453,44 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { addComposingMsgView() } } - // received message avatar - Observable<(Data?, String)>.combineLatest(conversationViewModel.profileImageData.asObservable(), - conversationViewModel.userName.asObservable(), - conversationViewModel.displayName.asObservable()) { profileImage, username, displayName in - if let displayName = displayName, !displayName.isEmpty { - return (profileImage, displayName) - } - return (profileImage, username) - } - .observeOn(MainScheduler.instance) - .startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value)) - .subscribe({ [weak self] profileData -> Void in - guard let data = profileData.element?.1 else { return } - self?.avatarView - .subviews.forEach({ $0.removeFromSuperview() }) - self?.avatarView - .addSubview( - AvatarView(profileImageData: profileData.element?.0, - username: data, - size: 32)) - self?.avatarView.isHidden = !(item.sequencing == .lastOfSequence || item.sequencing == .singleMessage) - return - }) - .disposed(by: self.disposeBag) + + configureReceivedMessageAvatar(item.sequencing, conversationViewModel) } } + private func configureReceivedMessageAvatar(_ itemSequencing: MessageSequencing, _ conversationViewModel: ConversationViewModel) { + + Observable<(Data?, String)>.combineLatest(conversationViewModel.profileImageData.asObservable(), + conversationViewModel.bestName.asObservable()) { ($0, $1) } + .observeOn(MainScheduler.instance) + .startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value)) + .subscribe({ [weak self] profileData in + guard let data = profileData.element?.1 else { return } + self?.avatarView.subviews.forEach({ $0.removeFromSuperview() }) + if itemSequencing == .lastOfSequence || itemSequencing == .singleMessage { + self?.avatarView.addSubview(AvatarView(profileImageData: profileData.element?.0, username: data, size: 32)) + } + }) + .disposed(by: self.disposeBag) + } + + fileprivate func configureMessageReadAvatar(_ item: MessageViewModel, _ conversationViewModel: ConversationViewModel) { + + Observable<(Data?, String, Bool)>.combineLatest(conversationViewModel.profileImageData.asObservable(), + conversationViewModel.bestName.asObservable(), + item.displayReadIndicator.asObservable()) { ($0, $1, $2) } + .observeOn(MainScheduler.instance) + .startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value, item.displayReadIndicator.value)) + .subscribe({ [weak self] profileData in + guard let bestName = profileData.element?.1 else { return } + self?.messageReadIndicator?.subviews.forEach({ $0.removeFromSuperview() }) + if let displayReadIndicator = profileData.element?.2, displayReadIndicator { + self?.messageReadIndicator?.addSubview(AvatarView(profileImageData: profileData.element?.0, username: bestName, size: 12)) + } + }) + .disposed(by: self.disposeBag) + } + func addComposingMsgView() { self.composingMsg = UIView(frame: self.messageLabel.frame) let size: CGFloat = 10 diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift index 3e3a1bf16487bb3580ff7a41d2b8ae54e4178f39..2d75154ea9898501df6887b9f33c9e4316c202d4 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -5,6 +5,7 @@ * Author: Quentin Muret <quentin.muret@savoirfairelinux.com> * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,6 +48,7 @@ class ConversationViewController: UIViewController, var bottomOffset: CGFloat = 0 let scrollOffsetThreshold: CGFloat = 600 var bottomHeight: CGFloat = 0.00 + var isExecutingDeleteMessage: Bool = false @IBOutlet weak var currentCallButton: UIButton! @IBOutlet weak var currentCallLabel: UILabel! @@ -104,7 +106,7 @@ class ConversationViewController: UIViewController, self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor } - func importDocument() { + private func importDocument() { let documentPicker = UIDocumentPickerViewController(documentTypes: ["public.item"], in: .import) documentPicker.delegate = self documentPicker.modalPresentationStyle = .formSheet @@ -128,7 +130,7 @@ class ConversationViewController: UIViewController, } } - func showNoPermissionsAlert(title: String) { + private func showNoPermissionsAlert(title: String) { let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { (_: UIAlertAction!) -> Void in } alert.addAction(okAction) @@ -664,9 +666,12 @@ class ConversationViewController: UIViewController, } fileprivate func scrollToBottomIfNeed() { - if self.isBottomContentOffset { + if self.isBottomContentOffset && !self.isExecutingDeleteMessage { self.scrollToBottom(animated: false) } + if self.isExecutingDeleteMessage { + self.isExecutingDeleteMessage = false + } } fileprivate func scrollToBottom(animated: Bool) { @@ -678,7 +683,7 @@ class ConversationViewController: UIViewController, } fileprivate var isBottomContentOffset: Bool { - updateBottomOffset() + self.updateBottomOffset() let offset = abs((self.tableView.contentOffset.y + self.tableView.contentInset.top) - bottomOffset) return offset <= scrollOffsetThreshold } @@ -733,7 +738,7 @@ class ConversationViewController: UIViewController, } // MARK: - message formatting - func computeSequencing() { + private func computeSequencing() { var lastShownTime: Date? for (index, messageViewModel) in self.messageViewModels!.enumerated() { // time labels @@ -759,7 +764,7 @@ class ConversationViewController: UIViewController, } } - func getMessageSequencing(forIndex index: Int) -> MessageSequencing { + private func getMessageSequencing(forIndex index: Int) -> MessageSequencing { if let models = self.messageViewModels { let messageItem = models[index] let msgOwner = messageItem.bubblePosition() @@ -800,25 +805,22 @@ class ConversationViewController: UIViewController, return MessageSequencing.unknown } - func getTimeLabelString(forTime time: Date) -> String { + private func getTimeLabelString(forTime time: Date) -> String { // get the current time let currentDateTime = Date() // prepare formatter let dateFormatter = DateFormatter() - if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame { - if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame { - if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame { - // age: [0, received the previous day[ - dateFormatter.dateFormat = "h:mma" - } else { - // age: [received the previous day, received 7 days ago[ - dateFormatter.dateFormat = "E h:mma" - } - } else { - // age: [received 7 days ago, received the previous year[ - dateFormatter.dateFormat = "MMM d, h:mma" - } + + if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame { + // age: [0, received the previous day[ + dateFormatter.dateFormat = "h:mma" + } else if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame { + // age: [received the previous day, received 7 days ago[ + dateFormatter.dateFormat = "E h:mma" + } else if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame { + // age: [received 7 days ago, received the previous year[ + dateFormatter.dateFormat = "MMM d, h:mma" } else { // age: [received the previous year, inf[ dateFormatter.dateFormat = "MMM d, yyyy h:mma" @@ -938,109 +940,128 @@ extension ConversationViewController: UITableViewDataSource { return self.messageViewModels?.count ?? 0 } - // swiftlint:disable cyclomatic_complexity func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if let item = self.messageViewModels?[indexPath.row] { - var type = MessageCell.self - if item.isTransfer { - type = item.bubblePosition() == .received ? MessageCellDataTransferReceived.self : MessageCellDataTransferSent.self - } else { - type = item.bubblePosition() == .received ? MessageCellReceived.self : - item.bubblePosition() == .sent ? MessageCellSent.self : - item.bubblePosition() == .generated ? MessageCellGenerated.self : - MessageCellGenerated.self - } - if item.message.incoming && item.message.status != .displayed && !item.message.isTransfer { - self.viewModel - .setMessageAsRead(daemonId: item.message.daemonId, - messageId: item.message.messageId) + + if item.message.incoming && + item.message.status != .displayed && + !item.message.isTransfer { + self.viewModel.setMessageAsRead(daemonId: item.message.daemonId, + messageId: item.message.messageId) } - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: type) + + let cellType = { (bubblePosition: BubblePosition, isTransfer: Bool) -> MessageCell.Type in + switch bubblePosition { + case .received: return isTransfer ? MessageCellDataTransferReceived.self : MessageCellReceived.self + case .sent: return isTransfer ? MessageCellDataTransferSent.self : MessageCellSent.self + case .generated: return MessageCellGenerated.self + } + }(item.bubblePosition(), item.isTransfer) + + let cell = tableView.dequeueReusableCell(for: indexPath, cellType: cellType) cell.configureFromItem(viewModel, self.messageViewModels, cellForRowAt: indexPath) - if item.isTransfer { - cell.acceptButton?.setTitle(L10n.DataTransfer.readableStatusAccept, for: .normal) - item.lastTransferStatus = .unknown - changeTransferStatus(cell, nil, item.message.transferStatus, item, viewModel) - item.transferStatus.asObservable() - .observeOn(MainScheduler.instance) - .filter { - return $0 != DataTransferStatus.unknown && $0 != item.lastTransferStatus && $0 != item.initialTransferStatus } - .subscribe(onNext: { [weak self, weak tableView, weak cell] status in - guard let cell = cell else {return} - guard let currentIndexPath = tableView?.indexPath(for: cell) else { return } - guard let transferId = item.daemonId else { return } - guard let model = self?.viewModel else { return } - self?.log.info("Transfer status change from: \(item.lastTransferStatus.description) to: \(status.description) for transferId: \(transferId) cell row: \(currentIndexPath.row)") - if item.bubblePosition() == .sent && item.shouldDisplayTransferedImage { - cell.displayTransferedImage(message: item, conversationID: model.conversation.value.conversationId, accountId: model.conversation.value.accountId) - } else { - self?.changeTransferStatus(cell, currentIndexPath, status, item, model) - cell.stopProgressMonitor() + transferCellSetup(item, cell, tableView, indexPath) + deleteCellSetup(cell) + + return cell + } + return tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self) + } + + private func deleteCellSetup(_ cell: MessageCell) { + cell.deleteMessage + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self, weak cell] (shouldDelete) in + guard shouldDelete, let self = self, let cell = cell, let messageId = cell.messageId else { return } + self.isExecutingDeleteMessage = true + self.viewModel.deleteMessage(messageId: messageId) + }) + .disposed(by: cell.disposeBag) + } + + // swiftlint:disable cyclomatic_complexity + private func transferCellSetup(_ item: MessageViewModel, _ cell: MessageCell, _ tableView: UITableView, _ indexPath: IndexPath) { + if item.isTransfer { + cell.acceptButton?.setTitle(L10n.DataTransfer.readableStatusAccept, for: .normal) + item.lastTransferStatus = .unknown + changeTransferStatus(cell, nil, item.message.transferStatus, item, viewModel) + item.transferStatus.asObservable() + .observeOn(MainScheduler.instance) + .filter { + return $0 != DataTransferStatus.unknown && $0 != item.lastTransferStatus && $0 != item.initialTransferStatus } + .subscribe(onNext: { [weak self, weak tableView, weak cell] status in + guard let cell = cell else {return} + guard let currentIndexPath = tableView?.indexPath(for: cell) else { return } + guard let transferId = item.daemonId else { return } + guard let model = self?.viewModel else { return } + self?.log.info("Transfer status change from: \(item.lastTransferStatus.description) to: \(status.description) for transferId: \(transferId) cell row: \(currentIndexPath.row)") + if item.bubblePosition() == .sent && item.shouldDisplayTransferedImage { + cell.displayTransferedImage(message: item, conversationID: model.conversation.value.conversationId, accountId: model.conversation.value.accountId) + } else { + self?.changeTransferStatus(cell, currentIndexPath, status, item, model) + cell.stopProgressMonitor() + } + item.lastTransferStatus = status + item.initialTransferStatus = status + tableView?.reloadData() + }) + .disposed(by: cell.disposeBag) + + cell.cancelButton.rx.tap + .subscribe(onNext: { [weak self, weak tableView, weak cell] _ in + guard let cell = cell else {return} + guard let transferId = item.daemonId else { return } + self?.log.info("canceling transferId \(transferId)") + _ = self?.viewModel.cancelTransfer(transferId: transferId) + item.initialTransferStatus = .canceled + item.message.transferStatus = .canceled + cell.stopProgressMonitor() + tableView?.reloadData() + }) + .disposed(by: cell.disposeBag) + cell.playerHeight + .asObservable() + .share() + .observeOn(MainScheduler.instance) + .subscribe(onNext: {[weak tableView] height in + if height > 0 { + UIView.performWithoutAnimation { + guard let sectionNumber = tableView?.numberOfSections, + let rowNumber = tableView?.numberOfRows(inSection: indexPath.section) else {return} + if indexPath.section < sectionNumber && indexPath.section >= 0 { + if indexPath.row < rowNumber && + indexPath.row >= 0 && + indexPath.row != tableView?.numberOfRows(inSection: indexPath.section) { + tableView? + .reloadItemsAtIndexPaths([indexPath], + animationStyle: .top) + } + } } - item.lastTransferStatus = status - item.initialTransferStatus = status - tableView?.reloadData() - }) - .disposed(by: cell.disposeBag) + } + }).disposed(by: cell.disposeBag) - cell.cancelButton.rx.tap + if item.bubblePosition() == .received { + cell.acceptButton?.rx.tap .subscribe(onNext: { [weak self, weak tableView, weak cell] _ in guard let cell = cell else {return} guard let transferId = item.daemonId else { return } - self?.log.info("canceling transferId \(transferId)") - _ = self?.viewModel.cancelTransfer(transferId: transferId) - item.initialTransferStatus = .canceled - item.message.transferStatus = .canceled - cell.stopProgressMonitor() - tableView?.reloadData() + self?.log.info("accepting transferId \(transferId)") + if self?.viewModel.acceptTransfer(transferId: transferId, interactionID: item.messageId, messageContent: &item.message.content) != .success { + _ = self?.viewModel.cancelTransfer(transferId: transferId) + item.initialTransferStatus = .canceled + item.message.transferStatus = .canceled + cell.stopProgressMonitor() + tableView?.reloadData() + } }) .disposed(by: cell.disposeBag) - cell.playerHeight - .asObservable() - .share() - .observeOn(MainScheduler.instance) - .subscribe(onNext: {[weak tableView] height in - if height > 0 { - UIView.performWithoutAnimation { - guard let sectionNumber = tableView?.numberOfSections, - let rowNumber = tableView?.numberOfRows(inSection: indexPath.section) else {return} - if indexPath.section < sectionNumber && indexPath.section >= 0 { - if indexPath.row < rowNumber && - indexPath.row >= 0 && - indexPath.row != tableView?.numberOfRows(inSection: indexPath.section) { - tableView? - .reloadItemsAtIndexPaths([indexPath], - animationStyle: .top) - } - } - } - } - }).disposed(by: cell.disposeBag) - - if item.bubblePosition() == .received { - cell.acceptButton?.rx.tap - .subscribe(onNext: { [weak self, weak tableView, weak cell] _ in - guard let cell = cell else {return} - guard let transferId = item.daemonId else { return } - self?.log.info("accepting transferId \(transferId)") - if self?.viewModel.acceptTransfer(transferId: transferId, interactionID: item.messageId, messageContent: &item.message.content) != .success { - _ = self?.viewModel.cancelTransfer(transferId: transferId) - item.initialTransferStatus = .canceled - item.message.transferStatus = .canceled - cell.stopProgressMonitor() - tableView?.reloadData() - } - }) - .disposed(by: cell.disposeBag) - } - if item.message.transferStatus == .success { - self.addShareAction(cell: cell, item: item) - } } - return cell + if item.message.transferStatus == .success { + self.addShareAction(cell: cell, item: item) + } } - return tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self) } } // swiftlint:enable type_body_length diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index e5a2a19b03a90718dd665757fea7f076a91b0c73..0181dbf4881a490b3b60290010d556733a1d7017 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -5,6 +5,7 @@ * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * Author: Quentin Muret <quentin.muret@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -277,11 +278,8 @@ class ConversationViewModel: Stateable, ViewModel { return Observable .combineLatest(userName.asObservable(), displayName.asObservable()) {(userName, displayname) in - guard let name = displayname, - !name.isEmpty else { - return userName - } - return name + guard let displayname = displayname, !displayname.isEmpty else { return userName } + return displayname } }() @@ -403,6 +401,17 @@ class ConversationViewModel: Stateable, ViewModel { }.first?.status = .displayed } + func deleteMessage(messageId: Int64) { + guard let account = self.accountService.currentAccount else { return } + self.conversationsService + .deleteMessage(messagesId: messageId, accountId: account.id) + .subscribe(onCompleted: { [weak self] in + self?.log.debug("Messages was deleted") + }) + .disposed(by: disposeBag) + self.messages.value.removeAll(where: { $0.messageId == messageId }) + } + fileprivate var unreadMessagesCount: Int { let unreadMessages = self.conversation.value.messages .filter({ message in diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift index b7ae436a7077f6705d23b8b7201a52c8fe07e072..73a2e7c08f6da53e1b0db6873343b56a54de7807 100644 --- a/Ring/Ring/Services/ConversationsService.swift +++ b/Ring/Ring/Services/ConversationsService.swift @@ -3,6 +3,7 @@ * * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> * Author: Quentin Muret <quentin.muret@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -359,6 +360,17 @@ class ConversationsService { }) } + func deleteMessage(messagesId: Int64, accountId: String) -> Completable { + return Completable.create(subscribe: { [unowned self] completable in + self.dbManager + .deleteMessage(messagesId: messagesId, accountId: accountId) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe(onCompleted: { completable(.completed) }, onError: { error in completable(.error(error))}) + .disposed(by: self.disposeBag) + return Disposables.create { } + }) + } + func getProfile(uri: String, accountId: String) -> Observable<Profile> { return self.dbManager.profileObservable(for: uri, createIfNotExists: false, accountId: accountId) }