300 lines
10 KiB
Swift
300 lines
10 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
// Represents which interactions are currently selected during multi-select.
|
|
public class CVCellSelection {
|
|
private var _selectedInteractionIds = Set<String>()
|
|
|
|
fileprivate func add(_ interactionId: String) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(!isSelected(interactionId))
|
|
|
|
_selectedInteractionIds.insert(interactionId)
|
|
}
|
|
|
|
fileprivate func remove(_ interactionId: String) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(isSelected(interactionId))
|
|
|
|
_selectedInteractionIds.remove(interactionId)
|
|
}
|
|
|
|
fileprivate func isSelected(_ interactionId: String) -> Bool {
|
|
AssertIsOnMainThread()
|
|
|
|
return _selectedInteractionIds.contains(interactionId)
|
|
}
|
|
|
|
func reset() {
|
|
AssertIsOnMainThread()
|
|
|
|
_selectedInteractionIds.removeAll()
|
|
}
|
|
|
|
fileprivate var selectedInteractionIds: Set<String> {
|
|
AssertIsOnMainThread()
|
|
|
|
return _selectedInteractionIds
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ConversationViewController {
|
|
|
|
@objc
|
|
public func addToSelection(_ interactionId: String) {
|
|
cellSelection.add(interactionId)
|
|
// TODO: Update cells/selection view?
|
|
}
|
|
|
|
@objc
|
|
public func removeFromSelection(_ interactionId: String) {
|
|
cellSelection.remove(interactionId)
|
|
// TODO: Update cells/selection view?
|
|
}
|
|
|
|
@objc
|
|
public func isMessageSelected(_ interaction: TSInteraction) -> Bool {
|
|
cellSelection.isSelected(interaction.uniqueId)
|
|
}
|
|
fileprivate var selectedInteractionIds: Set<String> { cellSelection.selectedInteractionIds }
|
|
|
|
func clearSelection() {
|
|
cellSelection.reset()
|
|
clearCollectionViewSelection()
|
|
}
|
|
|
|
func clearCollectionViewSelection() {
|
|
guard let selectedIndices = collectionView.indexPathsForSelectedItems else {
|
|
owsFailDebug("selectedIndices was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
for index in selectedIndices {
|
|
collectionView.deselectItem(at: index, animated: false)
|
|
guard let cell = collectionView.cellForItem(at: index) else {
|
|
continue
|
|
}
|
|
cell.isSelected = false
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func buildSelectionToolbar() -> MessageActionsToolbar {
|
|
let deleteSelectedMessages = MessageAction(
|
|
.delete,
|
|
accessibilityLabel: NSLocalizedString("MESSAGE_ACTION_DELETE_SELECTED_MESSAGES",
|
|
comment: "accessibility label"),
|
|
accessibilityIdentifier: UIView.accessibilityIdentifier(containerName: "message_action",
|
|
name: "delete_selected_messages"),
|
|
block: { [weak self] _ in self?.didTapDeleteSelectedItems() }
|
|
)
|
|
|
|
let toolbar = MessageActionsToolbar(actions: [deleteSelectedMessages])
|
|
toolbar.actionDelegate = self
|
|
return toolbar
|
|
}
|
|
|
|
func didTapDeleteSelectedItems() {
|
|
let selectedInteractionIds = self.selectedInteractionIds
|
|
|
|
let message: String
|
|
if selectedInteractionIds.count > 1 {
|
|
let messageFormat = NSLocalizedString("DELETE_SELECTED_MESSAGES_IN_CONVERSATION_ALERT_FORMAT",
|
|
comment: "action sheet body. Embeds {{number of selected messages}} which will be deleted.")
|
|
message = String(format: messageFormat, selectedInteractionIds.count)
|
|
} else {
|
|
message = NSLocalizedString("DELETE_SELECTED_SINGLE_MESSAGES_IN_CONVERSATION_ALERT_FORMAT",
|
|
comment: "action sheet body")
|
|
}
|
|
let alert = ActionSheetController(title: nil, message: message)
|
|
alert.addAction(OWSActionSheets.cancelAction)
|
|
|
|
let delete = ActionSheetAction(title: CommonStrings.deleteForMeButton, style: .destructive) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] modalActivityIndicator in
|
|
guard let self = self else { return }
|
|
|
|
self.deleteSelectedItems(selectedInteractionIds: selectedInteractionIds)
|
|
|
|
DispatchQueue.main.async {
|
|
self.clearSelection()
|
|
modalActivityIndicator.dismiss {
|
|
self.uiMode = .normal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
alert.addAction(delete)
|
|
present(alert, animated: true)
|
|
}
|
|
|
|
private func deleteSelectedItems(selectedInteractionIds: Set<String>) {
|
|
databaseStorage.write { transaction in
|
|
for interactionId in selectedInteractionIds {
|
|
guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId,
|
|
transaction: transaction) else {
|
|
owsFailDebug("Missing interaction.")
|
|
continue
|
|
}
|
|
interaction.anyRemove(transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func updateSelectionButtons() {
|
|
guard let deleteButton = selectionToolbar.buttonItem(for: .delete) else {
|
|
owsFailDebug("deleteButton was unexpectedly nil")
|
|
return
|
|
}
|
|
deleteButton.isEnabled = selectedInteractionIds.count > 0
|
|
}
|
|
|
|
@objc
|
|
public func ensureSelectionViewState() {
|
|
guard isShowingSelectionUI else {
|
|
return
|
|
}
|
|
clearCollectionViewSelection()
|
|
|
|
let selectedInteractionIds = self.selectedInteractionIds
|
|
let selectedIndexPaths = selectedInteractionIds.compactMap { interactionId in
|
|
indexPath(forInteractionUniqueId: interactionId)
|
|
}
|
|
let indexPaths = collectionView.indexPathsForVisibleItems
|
|
for indexPath in indexPaths {
|
|
let isSelected = selectedIndexPaths.contains(indexPath)
|
|
if isSelected {
|
|
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
|
|
} else {
|
|
collectionView.deselectItem(at: indexPath, animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
class func consecutivelyGrouped(indexPaths: [IndexPath]) -> [[IndexPath]] {
|
|
let sorted = indexPaths.sorted { lhs, rhs in
|
|
if lhs.section == rhs.section {
|
|
return lhs.row < rhs.row
|
|
} else {
|
|
return lhs.section < rhs.section
|
|
}
|
|
}
|
|
|
|
var consecutiveIndexPaths: [[IndexPath]] = []
|
|
var previousIndexPath: IndexPath?
|
|
for indexPath in sorted {
|
|
defer {
|
|
previousIndexPath = indexPath
|
|
}
|
|
|
|
guard let previousIndexPath = previousIndexPath else {
|
|
consecutiveIndexPaths.append([indexPath])
|
|
continue
|
|
}
|
|
|
|
guard previousIndexPath.section == indexPath.section else {
|
|
consecutiveIndexPaths.append([indexPath])
|
|
continue
|
|
}
|
|
|
|
guard previousIndexPath.row + 1 == indexPath.row else {
|
|
consecutiveIndexPaths.append([indexPath])
|
|
continue
|
|
}
|
|
|
|
let lastIndex = consecutiveIndexPaths.endIndex - 1
|
|
consecutiveIndexPaths[lastIndex].append(indexPath)
|
|
}
|
|
|
|
return consecutiveIndexPaths
|
|
}
|
|
|
|
@objc
|
|
public func didSelectMessage(_ message: CVItemViewModel) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(isShowingSelectionUI)
|
|
|
|
let interactionId = message.interaction.uniqueId
|
|
guard let indexPath = indexPath(forInteractionUniqueId: interactionId) else {
|
|
owsFailDebug("indexPath was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
collectionView.selectItem(at: indexPath,
|
|
animated: false,
|
|
// TODO: Is there a better way to indicate .none?
|
|
scrollPosition: UICollectionView.ScrollPosition(rawValue: 0))
|
|
addToSelection(interactionId)
|
|
|
|
updateSelectionButtons()
|
|
}
|
|
|
|
@objc
|
|
public func didDeselectMessage(_ message: CVItemViewModel) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(isShowingSelectionUI)
|
|
|
|
let interactionId = message.interaction.uniqueId
|
|
guard let indexPath = indexPath(forInteractionUniqueId: interactionId) else {
|
|
owsFailDebug("indexPath was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
collectionView.deselectItem(at: indexPath, animated: false)
|
|
removeFromSelection(interactionId)
|
|
|
|
updateSelectionButtons()
|
|
}
|
|
}
|
|
|
|
// MARK: - Selection
|
|
|
|
extension ConversationViewController {
|
|
|
|
@objc
|
|
var cancelSelectionBarButtonItem: UIBarButtonItem {
|
|
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancelSelection))
|
|
}
|
|
|
|
@objc
|
|
var deleteAllBarButtonItem: UIBarButtonItem {
|
|
let title = NSLocalizedString("CONVERSATION_VIEW_DELETE_ALL_MESSAGES", comment: "button text to delete all items in the current conversation")
|
|
return UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(didTapDeleteAll))
|
|
}
|
|
|
|
@objc
|
|
func didTapCancelSelection() {
|
|
clearSelection()
|
|
uiMode = .normal
|
|
}
|
|
|
|
@objc
|
|
func didTapDeleteAll() {
|
|
let thread = self.thread
|
|
let alert = ActionSheetController(title: nil, message: NSLocalizedString("DELETE_ALL_MESSAGES_IN_CONVERSATION_ALERT_BODY", comment: "action sheet body"))
|
|
alert.addAction(OWSActionSheets.cancelAction)
|
|
let deleteTitle = NSLocalizedString("DELETE_ALL_MESSAGES_IN_CONVERSATION_BUTTON", comment: "button text")
|
|
let delete = ActionSheetAction(title: deleteTitle, style: .destructive) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] modalActivityIndicator in
|
|
guard let self = self else { return }
|
|
self.databaseStorage.write {
|
|
thread.removeAllThreadInteractions(transaction: $0)
|
|
}
|
|
DispatchQueue.main.async {
|
|
self.clearSelection()
|
|
modalActivityIndicator.dismiss {
|
|
self.uiMode = .normal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
alert.addAction(delete)
|
|
present(alert, animated: true)
|
|
}
|
|
}
|