From ff6d52dea7e5aa60470b1946f9f4f7531a4be7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20B=C3=B6ttcher?= Date: Tue, 7 Dec 2021 14:48:55 +0100 Subject: [PATCH 1/4] thread swipe gestures can be applied to search results --- Signal.xcodeproj/project.pbxproj | 4 + .../ConversationSearchViewController.swift | 69 ++++- .../HomeView/HVTableDataSource.swift | 132 +-------- .../HomeView/HomeViewCell.swift | 4 +- .../HomeView/HomeViewController+Actions.swift | 140 ---------- .../HomeView/ThreadSwipeHandler.swift | 256 ++++++++++++++++++ 6 files changed, 337 insertions(+), 268 deletions(-) create mode 100644 Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c744acd2d9..dbdc5d1e87 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 06289300DC49EDEA6FEC730C /* Pods_SignalPerformanceTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C61A9604F0FC0D258C8CE27F /* Pods_SignalPerformanceTests.framework */; }; + 1477630B275E20D700D1067E /* ThreadSwipeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1477630A275E20D700D1067E /* ThreadSwipeHandler.swift */; }; 1704690A25D4C326000793D8 /* SignalAttachmentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704690925D4C2E6000793D8 /* SignalAttachmentTest.swift */; }; 1704690C25D4C92B000793D8 /* test-jpg-rotated.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 1704690B25D4C92B000793D8 /* test-jpg-rotated.jpg */; }; 173878BE256341BB00AD39C7 /* SessionMigrationPerfTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173878BD256341BB00AD39C7 /* SessionMigrationPerfTest.swift */; }; @@ -1154,6 +1155,7 @@ 02E4AF4827F1AD3630C8B77B /* Pods-SignalUITests.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUITests.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUITests/Pods-SignalUITests.profiling.xcconfig"; sourceTree = ""; }; 0435CB909E03469591D260F3 /* Pods-SignalUI.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUI.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUI/Pods-SignalUI.testable release.xcconfig"; sourceTree = ""; }; 10AE4264D3E52937D8964A86 /* Pods-SignalMessaging.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.profiling.xcconfig"; sourceTree = ""; }; + 1477630A275E20D700D1067E /* ThreadSwipeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSwipeHandler.swift; sourceTree = ""; }; 14FD26DD3B40616C54CDBF1A /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 1704690925D4C2E6000793D8 /* SignalAttachmentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAttachmentTest.swift; sourceTree = ""; }; 1704690B25D4C92B000793D8 /* test-jpg-rotated.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "test-jpg-rotated.jpg"; sourceTree = ""; }; @@ -2706,6 +2708,7 @@ 34E95C1D269F4F4F004807EC /* HVRenderState.swift */, 34E95C20269F4F4F004807EC /* HVTableDataSource.swift */, 34E95C1F269F4F4F004807EC /* HVViewState.swift */, + 1477630A275E20D700D1067E /* ThreadSwipeHandler.swift */, ); path = HomeView; sourceTree = ""; @@ -6325,6 +6328,7 @@ 887B380A25F0427F00685845 /* NotificationSettingsViewController.swift in Sources */, 3426A37025631C250036407F /* CVComponentLinkPreview.swift in Sources */, 45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */, + 1477630B275E20D700D1067E /* ThreadSwipeHandler.swift in Sources */, 881BB28A2743531D00B609B8 /* BadgeDetailsSheet.swift in Sources */, 887B380825F0355700685845 /* ChatsSettingsViewController.swift in Sources */, 8847E6F226A0EFBD0063E319 /* AvatarEditViewController.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index e729d670d1..f7161e195c 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -255,14 +255,16 @@ public class ConversationSearchViewController: UITableViewController { } } - private func cellContentToken(forConfiguration configuration: HomeViewCell.Configuration) -> HVCellContentToken { + private func cellContentToken(forConfiguration configuration: HomeViewCell.Configuration, useCache: Bool = true) -> HVCellContentToken { AssertIsOnMainThread() // If we have an existing HVCellContentToken, use it. // Cell measurement/arrangement is expensive. let cacheKey = "\(configuration.thread.threadRecord.uniqueId).\(configuration.overrideSnippet?.string ?? "")" - if let cellContentToken = cellContentCache.get(key: cacheKey) { - return cellContentToken + if useCache { + if let cellContentToken = cellContentCache.get(key: cacheKey) { + return cellContentToken + } } let cellContentToken = HomeViewCell.buildCellContentToken(forConfiguration: configuration) @@ -472,6 +474,34 @@ public class ConversationSearchViewController: UITableViewController { } } + public override func tableView(_ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + AssertIsOnMainThread() + + guard let searchSection = SearchSection(rawValue: indexPath.section) else { + owsFailDebug("unknown section: \(indexPath.section)") + return nil + } + + let thread = searchSection == .contactThreads ? searchResultSet.contactThreads[indexPath.row].thread + : (searchSection == .groupThreads ? searchResultSet.groupThreads[indexPath.row].thread : nil) + return ThreadSwipeHandler(with: self).trailingSwipeActionsConfiguration(for: thread) + } + + public override func tableView(_ tableView: UITableView, + leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + AssertIsOnMainThread() + + guard let searchSection = SearchSection(rawValue: indexPath.section) else { + owsFailDebug("unknown section: \(indexPath.section)") + return nil + } + + let thread = searchSection == .contactThreads ? searchResultSet.contactThreads[indexPath.row].thread + : (searchSection == .groupThreads ? searchResultSet.groupThreads[indexPath.row].thread : nil) + return ThreadSwipeHandler(with: self).leadingSwipeActionsConfiguration(for: thread) + } + // MARK: Update Search Results var refreshTimer: Timer? @@ -514,7 +544,11 @@ public class ConversationSearchViewController: UITableViewController { reloadTableData() return } - guard lastSearchText != searchText else { + + // a database change will lead to a search with the lastSearchText + // in this case we only want to update the visible cells + let updateCellCandidates: [HomeViewCell]? = lastSearchText == searchText ? tableView.visibleCells.filter {$0 as? HomeViewCell != nil} as? [HomeViewCell] : nil + guard updateCellCandidates == nil || updateCellCandidates!.count > 0 else { // Ignoring redundant search. return } @@ -538,14 +572,37 @@ public class ConversationSearchViewController: UITableViewController { // Discard results from stale search. return } - self.searchResultSet = results - self.reloadTableData() + if let candidates = updateCellCandidates { + for cell in candidates { + if let (section, row) = self.getIndexPathFor(threadId: cell.thread?.uniqueId), let configuration = self.cellConfiguration(searchSection: section, row: row) { + cell.reset() + cell.configure(cellContentToken: self.cellContentToken(forConfiguration: configuration, useCache: false)) + } + } + } else { + self.reloadTableData() + } }) } // MARK: - + private func getIndexPathFor(threadId: String?) -> (SearchSection, Int)? { + if let threadId = threadId { + if let row = searchResultSet.contactThreads.map({$0.thread.threadRecord.uniqueId}).firstIndex(of: threadId) { + return (SearchSection.contactThreads, row) + } + if let row = searchResultSet.groupThreads.map({$0.thread.threadRecord.uniqueId}).firstIndex(of: threadId) { + return (SearchSection.groupThreads, row) + } + if let row = searchResultSet.messages.map({$0.thread.threadRecord.uniqueId}).firstIndex(of: threadId) { + return (SearchSection.messages, row) + } + } + return nil + } + private func isBlocked(thread: ThreadViewModel) -> Bool { owsAssertDebug(thread.homeViewInfo != nil) diff --git a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift index e3c225c703..925ba8f4bc 100644 --- a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift +++ b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift @@ -665,72 +665,18 @@ extension HVTableDataSource: UITableViewDataSource { return nil } - let muteAction = UIContextualAction(style: .normal, - title: nil) { [weak viewController] (_, _, completion) in - if threadViewModel.isMuted { - viewController?.unmuteThread(threadViewModel: threadViewModel) - } else { - viewController?.muteThreadWithSelection(threadViewModel: threadViewModel) + return ThreadSwipeHandler(with: viewController).trailingSwipeActionsConfiguration(for: threadViewModel, + archiveFromInbox: viewController.homeViewMode == .inbox, + closeConversationBlock: { [weak self] in + if let self = self, self.isConversationActive(forThread: threadViewModel.threadRecord) { + viewController.conversationSplitViewController?.closeSelectedConversation(animated: true) } - completion(false) - } - muteAction.backgroundColor = .ows_accentIndigo - muteAction.image = self.actionImage(name: threadViewModel.isMuted ? "bell-solid-24" : "bell-disabled-solid-24", - title: threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton) - muteAction.accessibilityLabel = threadViewModel.isMuted ? CommonStrings.unmuteButton :CommonStrings.muteButton - - let deleteAction = UIContextualAction(style: .destructive, - title: nil) { [weak viewController] (_, _, completion) in - viewController?.deleteThreadWithConfirmation(threadViewModel: threadViewModel) - completion(false) - } - deleteAction.backgroundColor = .ows_accentRed - deleteAction.image = self.actionImage(name: "trash-solid-24", - title: CommonStrings.deleteButton) - deleteAction.accessibilityLabel = CommonStrings.deleteButton - - let archiveAction = UIContextualAction(style: .normal, - title: nil) { [weak viewController] (_, _, completion) in - viewController?.archiveThread(threadViewModel: threadViewModel) - completion(false) - } - - let archiveTitle = (viewController.homeViewMode == .inbox - ? CommonStrings.archiveAction - : CommonStrings.unarchiveAction) - - archiveAction.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray45 : .ows_gray25 - archiveAction.image = self.actionImage(name: "archive-solid-24", - title: archiveTitle) - archiveAction.accessibilityLabel = archiveTitle - - // The first action will be auto-performed for "very long swipes". - return UISwipeActionsConfiguration(actions: [ archiveAction, deleteAction, muteAction ]) + }, updateUIBlock: { + viewController.updateViewState() + }) } } - private func actionImage(name imageName: String, title: String) -> UIImage? { - AssertIsOnMainThread() - - // We need to bake the title text into the image because `UIContextualAction` - // only displays title + image when the cell's height > 91. We want to always - // show both. - guard let image = UIImage(named: imageName) else { - owsFailDebug("Missing image.") - return nil - } - guard let image = image.withTitle(title, - font: UIFont.systemFont(ofSize: 13), - color: .ows_white, - maxTitleWidth: 68, - minimumScaleFactor: CGFloat(8) / CGFloat(13), - spacing: 4) else { - owsFailDebug("Missing image.") - return nil - } - return image.withRenderingMode(.alwaysTemplate) - } - public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { AssertIsOnMainThread() @@ -768,65 +714,11 @@ extension HVTableDataSource: UITableViewDataSource { owsFailDebug("Missing threadViewModel.") return nil } - let thread = threadViewModel.threadRecord - - let isThreadPinned = PinnedThreadManager.isThreadPinned(thread) - let pinnedStateAction: UIContextualAction - if isThreadPinned { - pinnedStateAction = UIContextualAction(style: .normal, - title: nil) { [weak viewController] (_, _, completion) in - viewController?.unpinThread(threadViewModel: threadViewModel) - completion(false) - } - pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) - pinnedStateAction.accessibilityLabel = CommonStrings.unpinAction - pinnedStateAction.image = actionImage(name: "unpin-solid-24", - title: CommonStrings.unpinAction) - } else { - pinnedStateAction = UIContextualAction(style: .destructive, - title: nil) { [weak viewController] (_, _, completion) in - completion(false) - viewController?.pinThread(threadViewModel: threadViewModel) - } - pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) - pinnedStateAction.accessibilityLabel = CommonStrings.pinAction - pinnedStateAction.image = actionImage(name: "pin-solid-24", - title: CommonStrings.pinAction) + guard let viewController = self.viewController else { + owsFailDebug("Missing viewController.") + return nil } - - let readStateAction: UIContextualAction - if threadViewModel.hasUnreadMessages { - readStateAction = UIContextualAction(style: .destructive, - title: nil) { [weak viewController] (_, _, completion) in - completion(false) - // We delay here so the animation can play out before we - // reload the cell - DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { [weak viewController] in - viewController?.markThreadAsRead(threadViewModel: threadViewModel) - } - } - readStateAction.backgroundColor = .ows_accentBlue - readStateAction.accessibilityLabel = CommonStrings.readAction - readStateAction.image = actionImage(name: "read-solid-24", - title: CommonStrings.readAction) - } else { - readStateAction = UIContextualAction(style: .normal, - title: nil) { [weak viewController] (_, _, completion) in - completion(false) - // We delay here so the animation can play out before we - // reload the cell - DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { [weak viewController] in - viewController?.markThreadAsUnread(threadViewModel: threadViewModel) - } - } - readStateAction.backgroundColor = .ows_accentBlue - readStateAction.accessibilityLabel = CommonStrings.unreadAction - readStateAction.image = actionImage(name: "unread-solid-24", - title: CommonStrings.unreadAction) - } - - // The first action will be auto-performed for "very long swipes". - return UISwipeActionsConfiguration(actions: [ readStateAction, pinnedStateAction ]) + return ThreadSwipeHandler(with: viewController).leadingSwipeActionsConfiguration(for: threadViewModel) } } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift index 3e440677c2..03da146d49 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift @@ -121,7 +121,7 @@ public class HomeViewCell: UITableViewCell { } } private var cellContentToken: HVCellContentToken? - private var thread: TSThread? { + var thread: TSThread? { cellContentToken?.thread } @@ -850,7 +850,7 @@ public class HomeViewCell: UITableViewCell { reset() } - private func reset() { + func reset() { isCellVisible = false for cvview in cvviews { diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController+Actions.swift b/Signal/src/ViewControllers/HomeView/HomeViewController+Actions.swift index 8df3098c6d..b886091f5b 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController+Actions.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewController+Actions.swift @@ -3,149 +3,9 @@ // import Foundation -import SignalCoreKit extension HomeViewController { - func archiveThread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - // If this conversation is currently selected, close it. - if isConversationActive(forThread: threadViewModel.threadRecord) { - conversationSplitViewController?.closeSelectedConversation(animated: true) - } - - databaseStorage.write { transaction in - switch self.homeViewMode { - case .inbox: - threadViewModel.associatedData.updateWith(isArchived: true, - updateStorageService: true, - transaction: transaction) - case .archive: - threadViewModel.associatedData.updateWith(isArchived: false, - updateStorageService: true, - transaction: transaction) - } - } - updateViewState() - } - - func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", - comment: "Title for the 'conversation delete confirmation' alert."), - message: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", - comment: "Message for the 'conversation delete confirmation' alert.")) - alert.addAction(ActionSheetAction(title: CommonStrings.deleteButton, - style: .destructive) { [weak self] _ in - self?.deleteThread(threadViewModel: threadViewModel) - }) - alert.addAction(OWSActionSheets.cancelAction) - - presentActionSheet(alert) - } - - func deleteThread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - // If this conversation is currently selected, close it. - if isConversationActive(forThread: threadViewModel.threadRecord) { - conversationSplitViewController?.closeSelectedConversation(animated: true) - } - - databaseStorage.write { transaction in - threadViewModel.threadRecord.softDelete(with: transaction) - } - - // TODO: Rename this method. - updateViewState() - } - - func markThreadAsRead(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - databaseStorage.write { transaction in - threadViewModel.threadRecord.markAllAsRead(updateStorageService: true, transaction: transaction) - } - } - - func markThreadAsUnread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - databaseStorage.write { transaction in - threadViewModel.associatedData.updateWith(isMarkedUnread: true, updateStorageService: true, transaction: transaction) - } - } - - func muteThreadWithSelection(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_ALERT_TITLE", - comment: "Title for the 'conversation mute confirmation' alert.")) - for (title, seconds) in [ - (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1H", comment: "1 hour"), kHourInterval), - (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_8H", comment: "8 hours"), 8 * kHourInterval), - (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1D", comment: "1 day"), kDayInterval), - (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1W", comment: "1 week"), kWeekInterval), - (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_ALWAYS", comment: "Always"), -1)] { - alert.addAction(ActionSheetAction(title: title, style: .default) { [weak self] _ in - self?.muteThread(threadViewModel: threadViewModel, duration: seconds) - }) - } - alert.addAction(OWSActionSheets.cancelAction) - - presentActionSheet(alert) - } - - func muteThread(threadViewModel: ThreadViewModel, duration seconds: TimeInterval) { - AssertIsOnMainThread() - - databaseStorage.write { transaction in - let timeStamp = seconds < 0 - ? ThreadAssociatedData.alwaysMutedTimestamp - : (seconds == 0 ? 0 : Date.ows_millisecondTimestamp() + UInt64(seconds * 1000)) - threadViewModel.associatedData.updateWith(mutedUntilTimestamp: timeStamp, updateStorageService: true, transaction: transaction) - } - } - - func unmuteThread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - databaseStorage.write { transaction in - threadViewModel.associatedData.updateWith(mutedUntilTimestamp: Date.ows_millisecondTimestamp(), updateStorageService: true, transaction: transaction) - } - } - - func pinThread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - do { - try databaseStorage.write { transaction in - try PinnedThreadManager.pinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) - } - } catch { - if case PinnedThreadError.tooManyPinnedThreads = error { - OWSActionSheets.showActionSheet(title: NSLocalizedString("PINNED_CONVERSATION_LIMIT", - comment: "An explanation that you have already pinned the maximum number of conversations.")) - } else { - owsFailDebug("Error: \(error)") - } - } - } - - func unpinThread(threadViewModel: ThreadViewModel) { - AssertIsOnMainThread() - - do { - try databaseStorage.write { transaction in - try PinnedThreadManager.unpinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) - } - } catch { - owsFailDebug("Error: \(error)") - } - } - func selectPreviousConversation() { AssertIsOnMainThread() diff --git a/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift new file mode 100644 index 0000000000..0ca599541f --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift @@ -0,0 +1,256 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalMessaging +import UIKit + +class ThreadSwipeHandler { + let parent: UIViewController + + required init(with parent: UIViewController) { + self.parent = parent + } + + public func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, updatedHandler: (()->Void)? = nil) -> UISwipeActionsConfiguration? { + AssertIsOnMainThread() + + guard let threadViewModel = threadViewModel else { + return nil + } + + let thread = threadViewModel.threadRecord + let isThreadPinned = PinnedThreadManager.isThreadPinned(thread) + let pinnedStateAction: UIContextualAction + if isThreadPinned { + pinnedStateAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in + self?.unpinThread(threadViewModel: threadViewModel) + completion(false) + updatedHandler?() + } + pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) + pinnedStateAction.accessibilityLabel = CommonStrings.unpinAction + pinnedStateAction.image = actionImage(name: "unpin-solid-24", title: CommonStrings.unpinAction) + } else { + pinnedStateAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in + completion(false) + self.pinThread(threadViewModel: threadViewModel) + updatedHandler?() + } + pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) + pinnedStateAction.accessibilityLabel = CommonStrings.pinAction + pinnedStateAction.image = actionImage(name: "pin-solid-24", title: CommonStrings.pinAction) + } + + let readStateAction: UIContextualAction + if threadViewModel.hasUnreadMessages { + readStateAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in + completion(false) + self.markThreadAsRead(threadViewModel: threadViewModel) + updatedHandler?() + } + readStateAction.backgroundColor = .ows_accentBlue + readStateAction.accessibilityLabel = CommonStrings.readAction + readStateAction.image = actionImage(name: "read-solid-24", title: CommonStrings.readAction) + } else { + readStateAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in + completion(false) + self.markThreadAsUnread(threadViewModel: threadViewModel) + updatedHandler?() + } + readStateAction.backgroundColor = .ows_accentBlue + readStateAction.accessibilityLabel = CommonStrings.unreadAction + readStateAction.image = actionImage(name: "unread-solid-24", title: CommonStrings.unreadAction) + } + + // The first action will be auto-performed for "very long swipes". + return UISwipeActionsConfiguration(actions: [ readStateAction, pinnedStateAction ]) + } + + public func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, archiveFromInbox: Bool? = nil, closeConversationBlock: (()->Void)? = nil, updateUIBlock: (()->Void)? = nil) -> UISwipeActionsConfiguration? { + AssertIsOnMainThread() + + guard let threadViewModel = threadViewModel else { + return nil + } + + let muteAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in + if threadViewModel.isMuted { + self.unmuteThread(threadViewModel: threadViewModel) + } else { + self.muteThreadWithSelection(threadViewModel: threadViewModel) + } + completion(false) + } + muteAction.backgroundColor = .ows_accentIndigo + muteAction.image = actionImage(name: threadViewModel.isMuted ? "bell-solid-24" : "bell-disabled-solid-24", + title: threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton) + muteAction.accessibilityLabel = threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton + + let deleteAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in + self.deleteThreadWithConfirmation(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + completion(false) + } + deleteAction.backgroundColor = .ows_accentRed + deleteAction.image = actionImage(name: "trash-solid-24", title: CommonStrings.deleteButton) + deleteAction.accessibilityLabel = CommonStrings.deleteButton + + let archiveAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in + self.archiveThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + completion(false) + } + + let archiveTitle = threadViewModel.isArchived ? CommonStrings.unarchiveAction : CommonStrings.archiveAction + archiveAction.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray45 : .ows_gray25 + archiveAction.image = actionImage(name: "archive-solid-24", title: archiveTitle) + archiveAction.accessibilityLabel = archiveTitle + + // The first action will be auto-performed for "very long swipes". + return UISwipeActionsConfiguration(actions: [archiveAction, deleteAction, muteAction]) + } + + public func actionImage(name imageName: String, title: String) -> UIImage? { + AssertIsOnMainThread() + // We need to bake the title text into the image because `UIContextualAction` + // only displays title + image when the cell's height > 91. We want to always + // show both. + guard let image = UIImage(named: imageName) else { + owsFailDebug("Missing image.") + return nil + } + guard let image = image.withTitle(title, + font: UIFont.systemFont(ofSize: 13), + color: .ows_white, + maxTitleWidth: 68, + minimumScaleFactor: CGFloat(8) / CGFloat(13), + spacing: 4) else { + owsFailDebug("Missing image.") + return nil + } + return image.withRenderingMode(.alwaysTemplate) + } + + fileprivate func archiveThread(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + AssertIsOnMainThread() + + closeConversationBlock?() + parent.databaseStorage.write { transaction in + threadViewModel.associatedData.updateWith(isArchived: !threadViewModel.isArchived, + updateStorageService: true, + transaction: transaction) + } + updateUIBlock?() + } + + fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + AssertIsOnMainThread() + + let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", + comment: "Title for the 'conversation delete confirmation' alert."), + message: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", + comment: "Message for the 'conversation delete confirmation' alert.")) + alert.addAction(ActionSheetAction(title: CommonStrings.deleteButton, + style: .destructive) { [weak self] _ in + self?.deleteThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + }) + alert.addAction(OWSActionSheets.cancelAction) + + parent.presentActionSheet(alert) + } + + fileprivate func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + AssertIsOnMainThread() + + closeConversationBlock?() + parent.databaseStorage.write { transaction in + threadViewModel.threadRecord.softDelete(with: transaction) + } + updateUIBlock?() + } + + fileprivate func markThreadAsRead(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + parent.databaseStorage.write { transaction in + threadViewModel.threadRecord.markAllAsRead(updateStorageService: true, transaction: transaction) + } + } + + fileprivate func markThreadAsUnread(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + parent.databaseStorage.write { transaction in + threadViewModel.associatedData.updateWith(isMarkedUnread: true, updateStorageService: true, transaction: transaction) + } + } + + fileprivate func muteThreadWithSelection(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_ALERT_TITLE", + comment: "Title for the 'conversation mute confirmation' alert.")) + for (title, seconds) in [ + (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1H", comment: "1 hour"), kHourInterval), + (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_8H", comment: "8 hours"), 8 * kHourInterval), + (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1D", comment: "1 day"), kDayInterval), + (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_1W", comment: "1 week"), kWeekInterval), + (NSLocalizedString("CONVERSATION_MUTE_CONFIRMATION_OPTION_ALWAYS", comment: "Always"), -1)] { + alert.addAction(ActionSheetAction(title: title, style: .default) { [weak self] _ in + self?.muteThread(threadViewModel: threadViewModel, duration: seconds) + }) + } + alert.addAction(OWSActionSheets.cancelAction) + + parent.presentActionSheet(alert) + } + + fileprivate func muteThread(threadViewModel: ThreadViewModel, duration seconds: TimeInterval) { + AssertIsOnMainThread() + + parent.databaseStorage.write { transaction in + let timeStamp = seconds < 0 + ? ThreadAssociatedData.alwaysMutedTimestamp + : (seconds == 0 ? 0 : Date.ows_millisecondTimestamp() + UInt64(seconds * 1000)) + threadViewModel.associatedData.updateWith(mutedUntilTimestamp: timeStamp, updateStorageService: true, transaction: transaction) + } + } + + fileprivate func unmuteThread(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + parent.databaseStorage.write { transaction in + threadViewModel.associatedData.updateWith(mutedUntilTimestamp: Date.ows_millisecondTimestamp(), updateStorageService: true, transaction: transaction) + } + } + + fileprivate func pinThread(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + do { + try parent.databaseStorage.write { transaction in + try PinnedThreadManager.pinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) + } + } catch { + if case PinnedThreadError.tooManyPinnedThreads = error { + OWSActionSheets.showActionSheet(title: NSLocalizedString("PINNED_CONVERSATION_LIMIT", + comment: "An explanation that you have already pinned the maximum number of conversations.")) + } else { + owsFailDebug("Error: \(error)") + } + } + } + + fileprivate func unpinThread(threadViewModel: ThreadViewModel) { + AssertIsOnMainThread() + + do { + try parent.databaseStorage.write { transaction in + try PinnedThreadManager.unpinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) + } + } catch { + owsFailDebug("Error: \(error)") + } + } + +} From 6599e189f5e8258560016fef020eb7976db1acab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20B=C3=B6ttcher?= Date: Wed, 8 Dec 2021 11:42:14 +0100 Subject: [PATCH 2/4] refactored some details of ThreadSwipeHandler --- .../ConversationSearchViewController.swift | 37 +++++---- .../HomeView/HVTableDataSource.swift | 20 ++--- .../HomeView/HomeViewController.swift | 6 ++ .../HomeView/ThreadSwipeHandler.swift | 83 ++++++++++--------- 4 files changed, 79 insertions(+), 67 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index f7161e195c..5d4a4433e0 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -54,6 +54,7 @@ public class ConversationSearchViewController: UITableViewController { } private var hasThemeChanged = false + private lazy var threadSwipeHandler: ThreadSwipeHandler = { ThreadSwipeHandler(with: self) }() class var matchSnippetStyle: StringStyle { StringStyle( @@ -478,28 +479,14 @@ public class ConversationSearchViewController: UITableViewController { trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() - guard let searchSection = SearchSection(rawValue: indexPath.section) else { - owsFailDebug("unknown section: \(indexPath.section)") - return nil - } - - let thread = searchSection == .contactThreads ? searchResultSet.contactThreads[indexPath.row].thread - : (searchSection == .groupThreads ? searchResultSet.groupThreads[indexPath.row].thread : nil) - return ThreadSwipeHandler(with: self).trailingSwipeActionsConfiguration(for: thread) + return threadSwipeHandler.trailingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) } public override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() - guard let searchSection = SearchSection(rawValue: indexPath.section) else { - owsFailDebug("unknown section: \(indexPath.section)") - return nil - } - - let thread = searchSection == .contactThreads ? searchResultSet.contactThreads[indexPath.row].thread - : (searchSection == .groupThreads ? searchResultSet.groupThreads[indexPath.row].thread : nil) - return ThreadSwipeHandler(with: self).leadingSwipeActionsConfiguration(for: thread) + return threadSwipeHandler.leadingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) } // MARK: Update Search Results @@ -545,9 +532,12 @@ public class ConversationSearchViewController: UITableViewController { return } - // a database change will lead to a search with the lastSearchText + // a database change will lead to a search with the searchText=lastSearchText // in this case we only want to update the visible cells - let updateCellCandidates: [HomeViewCell]? = lastSearchText == searchText ? tableView.visibleCells.filter {$0 as? HomeViewCell != nil} as? [HomeViewCell] : nil + var updateCellCandidates: [HomeViewCell]? + if lastSearchText == searchText { + updateCellCandidates = tableView.visibleCells.filter {$0 as? HomeViewCell != nil} as? [HomeViewCell] + } guard updateCellCandidates == nil || updateCellCandidates!.count > 0 else { // Ignoring redundant search. return @@ -588,6 +578,17 @@ public class ConversationSearchViewController: UITableViewController { // MARK: - + private func getThreadViewModelFor(indexPath: IndexPath) -> ThreadViewModel? { + if let searchSection = SearchSection(rawValue: indexPath.section) { + if searchSection == .contactThreads { + return searchResultSet.contactThreads[indexPath.row].thread + } else if searchSection == .groupThreads { + return searchResultSet.groupThreads[indexPath.row].thread + } + } + return nil + } + private func getIndexPathFor(threadId: String?) -> (SearchSection, Int)? { if let threadId = threadId { if let row = searchResultSet.contactThreads.map({$0.thread.threadRecord.uniqueId}).firstIndex(of: threadId) { diff --git a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift index 925ba8f4bc..f0e6377636 100644 --- a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift +++ b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift @@ -12,7 +12,11 @@ public class HVTableDataSource: NSObject { public let tableView = HVTableView() @objc - public weak var viewController: HomeViewController? + public weak var viewController: HomeViewController? { + didSet { + threadSwipeHandler.parent = viewController + } + } fileprivate var splitViewController: UISplitViewController? { viewController?.splitViewController } @@ -24,6 +28,7 @@ public class HVTableDataSource: NSObject { fileprivate var lastReloadDate: Date? { tableView.lastReloadDate } fileprivate var lastPreloadCellDate: Date? + fileprivate let threadSwipeHandler = ThreadSwipeHandler() public required override init() { super.init() @@ -665,14 +670,11 @@ extension HVTableDataSource: UITableViewDataSource { return nil } - return ThreadSwipeHandler(with: viewController).trailingSwipeActionsConfiguration(for: threadViewModel, - archiveFromInbox: viewController.homeViewMode == .inbox, - closeConversationBlock: { [weak self] in + return threadSwipeHandler.trailingSwipeActionsConfiguration(for: threadViewModel, + closeConversationBlock: { [weak self] in if let self = self, self.isConversationActive(forThread: threadViewModel.threadRecord) { viewController.conversationSplitViewController?.closeSelectedConversation(animated: true) } - }, updateUIBlock: { - viewController.updateViewState() }) } } @@ -714,11 +716,7 @@ extension HVTableDataSource: UITableViewDataSource { owsFailDebug("Missing threadViewModel.") return nil } - guard let viewController = self.viewController else { - owsFailDebug("Missing viewController.") - return nil - } - return ThreadSwipeHandler(with: viewController).leadingSwipeActionsConfiguration(for: threadViewModel) + return threadSwipeHandler.leadingSwipeActionsConfiguration(for: threadViewModel) } } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.swift b/Signal/src/ViewControllers/HomeView/HomeViewController.swift index 1177e0317c..46159977f8 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.swift @@ -375,3 +375,9 @@ extension HomeViewController: BadgeExpirationSheetDelegate { func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) { } } + +extension HomeViewController: ThreadSwipeHandlerUIUpdater { + func updateUIAfterSwipeAction() { + updateViewState() + } +} diff --git a/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift index 0ca599541f..28fd0f33b4 100644 --- a/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift +++ b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift @@ -6,14 +6,21 @@ import Foundation import SignalMessaging import UIKit -class ThreadSwipeHandler { - let parent: UIViewController +protocol ThreadSwipeHandlerUIUpdater { + func updateUIAfterSwipeAction() +} - required init(with parent: UIViewController) { +class ThreadSwipeHandler { + weak var parent: UIViewController? + + init() { + } + + init(with parent: UIViewController?) { self.parent = parent } - public func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, updatedHandler: (()->Void)? = nil) -> UISwipeActionsConfiguration? { + public func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() guard let threadViewModel = threadViewModel else { @@ -27,16 +34,14 @@ class ThreadSwipeHandler { pinnedStateAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in self?.unpinThread(threadViewModel: threadViewModel) completion(false) - updatedHandler?() } pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) pinnedStateAction.accessibilityLabel = CommonStrings.unpinAction pinnedStateAction.image = actionImage(name: "unpin-solid-24", title: CommonStrings.unpinAction) } else { - pinnedStateAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in + pinnedStateAction = UIContextualAction(style: .destructive, title: nil) { [weak self] (_, _, completion) in completion(false) - self.pinThread(threadViewModel: threadViewModel) - updatedHandler?() + self?.pinThread(threadViewModel: threadViewModel) } pinnedStateAction.backgroundColor = UIColor(rgbHex: 0xff990a) pinnedStateAction.accessibilityLabel = CommonStrings.pinAction @@ -45,19 +50,17 @@ class ThreadSwipeHandler { let readStateAction: UIContextualAction if threadViewModel.hasUnreadMessages { - readStateAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in + readStateAction = UIContextualAction(style: .destructive, title: nil) { [weak self] (_, _, completion) in completion(false) - self.markThreadAsRead(threadViewModel: threadViewModel) - updatedHandler?() + self?.markThreadAsRead(threadViewModel: threadViewModel) } readStateAction.backgroundColor = .ows_accentBlue readStateAction.accessibilityLabel = CommonStrings.readAction readStateAction.image = actionImage(name: "read-solid-24", title: CommonStrings.readAction) } else { - readStateAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in + readStateAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in completion(false) - self.markThreadAsUnread(threadViewModel: threadViewModel) - updatedHandler?() + self?.markThreadAsUnread(threadViewModel: threadViewModel) } readStateAction.backgroundColor = .ows_accentBlue readStateAction.accessibilityLabel = CommonStrings.unreadAction @@ -68,18 +71,18 @@ class ThreadSwipeHandler { return UISwipeActionsConfiguration(actions: [ readStateAction, pinnedStateAction ]) } - public func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, archiveFromInbox: Bool? = nil, closeConversationBlock: (()->Void)? = nil, updateUIBlock: (()->Void)? = nil) -> UISwipeActionsConfiguration? { + public func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, closeConversationBlock: (() -> Void)? = nil) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() guard let threadViewModel = threadViewModel else { return nil } - let muteAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in + let muteAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in if threadViewModel.isMuted { - self.unmuteThread(threadViewModel: threadViewModel) + self?.unmuteThread(threadViewModel: threadViewModel) } else { - self.muteThreadWithSelection(threadViewModel: threadViewModel) + self?.muteThreadWithSelection(threadViewModel: threadViewModel) } completion(false) } @@ -88,16 +91,16 @@ class ThreadSwipeHandler { title: threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton) muteAction.accessibilityLabel = threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton - let deleteAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completion) in - self.deleteThreadWithConfirmation(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + let deleteAction = UIContextualAction(style: .destructive, title: nil) { [weak self] (_, _, completion) in + self?.deleteThreadWithConfirmation(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock) completion(false) } deleteAction.backgroundColor = .ows_accentRed deleteAction.image = actionImage(name: "trash-solid-24", title: CommonStrings.deleteButton) deleteAction.accessibilityLabel = CommonStrings.deleteButton - let archiveAction = UIContextualAction(style: .normal, title: nil) { (_, _, completion) in - self.archiveThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + let archiveAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in + self?.archiveThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock) completion(false) } @@ -131,19 +134,21 @@ class ThreadSwipeHandler { return image.withRenderingMode(.alwaysTemplate) } - fileprivate func archiveThread(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + fileprivate func archiveThread(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) { AssertIsOnMainThread() closeConversationBlock?() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(isArchived: !threadViewModel.isArchived, updateStorageService: true, transaction: transaction) } - updateUIBlock?() + if let parent = parent as? ThreadSwipeHandlerUIUpdater { + parent.updateUIAfterSwipeAction() + } } - fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) { AssertIsOnMainThread() let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", @@ -152,27 +157,29 @@ class ThreadSwipeHandler { comment: "Message for the 'conversation delete confirmation' alert.")) alert.addAction(ActionSheetAction(title: CommonStrings.deleteButton, style: .destructive) { [weak self] _ in - self?.deleteThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock, updateUIBlock: updateUIBlock) + self?.deleteThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock) }) alert.addAction(OWSActionSheets.cancelAction) - parent.presentActionSheet(alert) + parent?.presentActionSheet(alert) } - fileprivate func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (()->Void)?, updateUIBlock: (()->Void)?) { + fileprivate func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) { AssertIsOnMainThread() closeConversationBlock?() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in threadViewModel.threadRecord.softDelete(with: transaction) } - updateUIBlock?() + if let parent = parent as? ThreadSwipeHandlerUIUpdater { + parent.updateUIAfterSwipeAction() + } } fileprivate func markThreadAsRead(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in threadViewModel.threadRecord.markAllAsRead(updateStorageService: true, transaction: transaction) } } @@ -180,7 +187,7 @@ class ThreadSwipeHandler { fileprivate func markThreadAsUnread(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(isMarkedUnread: true, updateStorageService: true, transaction: transaction) } } @@ -202,13 +209,13 @@ class ThreadSwipeHandler { } alert.addAction(OWSActionSheets.cancelAction) - parent.presentActionSheet(alert) + parent?.presentActionSheet(alert) } fileprivate func muteThread(threadViewModel: ThreadViewModel, duration seconds: TimeInterval) { AssertIsOnMainThread() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in let timeStamp = seconds < 0 ? ThreadAssociatedData.alwaysMutedTimestamp : (seconds == 0 ? 0 : Date.ows_millisecondTimestamp() + UInt64(seconds * 1000)) @@ -219,7 +226,7 @@ class ThreadSwipeHandler { fileprivate func unmuteThread(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent.databaseStorage.write { transaction in + parent?.databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(mutedUntilTimestamp: Date.ows_millisecondTimestamp(), updateStorageService: true, transaction: transaction) } } @@ -228,7 +235,7 @@ class ThreadSwipeHandler { AssertIsOnMainThread() do { - try parent.databaseStorage.write { transaction in + try parent?.databaseStorage.write { transaction in try PinnedThreadManager.pinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) } } catch { @@ -245,7 +252,7 @@ class ThreadSwipeHandler { AssertIsOnMainThread() do { - try parent.databaseStorage.write { transaction in + try parent?.databaseStorage.write { transaction in try PinnedThreadManager.unpinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) } } catch { From bfe08e61c37b25eb09dc4ad6e32ea85821f95f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20B=C3=B6ttcher?= Date: Thu, 9 Dec 2021 08:32:40 +0100 Subject: [PATCH 3/4] refactored ThreadSwipeHandler: use of protocol extension, no more helper class --- .../ConversationSearchViewController.swift | 11 ++--- .../HomeView/HVTableDataSource.swift | 18 +++---- .../HomeView/HomeViewController.swift | 2 +- .../HomeView/ThreadSwipeHandler.swift | 49 +++++++------------ 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index 5d4a4433e0..33057d7f7b 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -11,7 +11,7 @@ public protocol ConversationSearchViewDelegate: AnyObject { } @objc -public class ConversationSearchViewController: UITableViewController { +public class ConversationSearchViewController: UITableViewController, ThreadSwipeHandler { // MARK: - @@ -54,7 +54,6 @@ public class ConversationSearchViewController: UITableViewController { } private var hasThemeChanged = false - private lazy var threadSwipeHandler: ThreadSwipeHandler = { ThreadSwipeHandler(with: self) }() class var matchSnippetStyle: StringStyle { StringStyle( @@ -477,16 +476,12 @@ public class ConversationSearchViewController: UITableViewController { public override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - AssertIsOnMainThread() - - return threadSwipeHandler.trailingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) + return trailingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) } public override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - AssertIsOnMainThread() - - return threadSwipeHandler.leadingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) + return leadingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath)) } // MARK: Update Search Results diff --git a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift index f8c5f8798f..b6823ac0bb 100644 --- a/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift +++ b/Signal/src/ViewControllers/HomeView/HVTableDataSource.swift @@ -12,11 +12,7 @@ public class HVTableDataSource: NSObject { public let tableView = HVTableView() @objc - public weak var viewController: HomeViewController? { - didSet { - threadSwipeHandler.parent = viewController - } - } + public weak var viewController: HomeViewController? fileprivate var splitViewController: UISplitViewController? { viewController?.splitViewController } @@ -28,7 +24,6 @@ public class HVTableDataSource: NSObject { fileprivate var lastReloadDate: Date? { tableView.lastReloadDate } fileprivate var lastPreloadCellDate: Date? - fileprivate let threadSwipeHandler = ThreadSwipeHandler() public required override init() { super.init() @@ -670,8 +665,8 @@ extension HVTableDataSource: UITableViewDataSource { return nil } - return threadSwipeHandler.trailingSwipeActionsConfiguration(for: threadViewModel, - closeConversationBlock: { [weak self] in + return viewController.trailingSwipeActionsConfiguration(for: threadViewModel, + closeConversationBlock: { [weak self] in if let self = self, self.isConversationActive(forThread: threadViewModel.threadRecord) { viewController.conversationSplitViewController?.closeSelectedConversation(animated: true) } @@ -716,7 +711,12 @@ extension HVTableDataSource: UITableViewDataSource { owsFailDebug("Missing threadViewModel.") return nil } - return threadSwipeHandler.leadingSwipeActionsConfiguration(for: threadViewModel) + guard let viewController = self.viewController else { + owsFailDebug("Missing viewController.") + return nil + } + + return viewController.leadingSwipeActionsConfiguration(for: threadViewModel) } } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.swift b/Signal/src/ViewControllers/HomeView/HomeViewController.swift index 3ddb7c130c..9a57b52ac6 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.swift @@ -406,7 +406,7 @@ extension HomeViewController: BadgeExpirationSheetDelegate { func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) { } } -extension HomeViewController: ThreadSwipeHandlerUIUpdater { +extension HomeViewController: ThreadSwipeHandler { func updateUIAfterSwipeAction() { updateViewState() } diff --git a/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift index 28fd0f33b4..0301ab6aaf 100644 --- a/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift +++ b/Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift @@ -6,21 +6,14 @@ import Foundation import SignalMessaging import UIKit -protocol ThreadSwipeHandlerUIUpdater { - func updateUIAfterSwipeAction() +@objc +protocol ThreadSwipeHandler { + @objc optional func updateUIAfterSwipeAction() } -class ThreadSwipeHandler { - weak var parent: UIViewController? +extension ThreadSwipeHandler where Self: UIViewController { - init() { - } - - init(with parent: UIViewController?) { - self.parent = parent - } - - public func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?) -> UISwipeActionsConfiguration? { + func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() guard let threadViewModel = threadViewModel else { @@ -71,7 +64,7 @@ class ThreadSwipeHandler { return UISwipeActionsConfiguration(actions: [ readStateAction, pinnedStateAction ]) } - public func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, closeConversationBlock: (() -> Void)? = nil) -> UISwipeActionsConfiguration? { + func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, closeConversationBlock: (() -> Void)? = nil) -> UISwipeActionsConfiguration? { AssertIsOnMainThread() guard let threadViewModel = threadViewModel else { @@ -113,7 +106,7 @@ class ThreadSwipeHandler { return UISwipeActionsConfiguration(actions: [archiveAction, deleteAction, muteAction]) } - public func actionImage(name imageName: String, title: String) -> UIImage? { + func actionImage(name imageName: String, title: String) -> UIImage? { AssertIsOnMainThread() // We need to bake the title text into the image because `UIContextualAction` // only displays title + image when the cell's height > 91. We want to always @@ -138,14 +131,12 @@ class ThreadSwipeHandler { AssertIsOnMainThread() closeConversationBlock?() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(isArchived: !threadViewModel.isArchived, updateStorageService: true, transaction: transaction) } - if let parent = parent as? ThreadSwipeHandlerUIUpdater { - parent.updateUIAfterSwipeAction() - } + updateUIAfterSwipeAction?() } fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) { @@ -161,25 +152,23 @@ class ThreadSwipeHandler { }) alert.addAction(OWSActionSheets.cancelAction) - parent?.presentActionSheet(alert) + presentActionSheet(alert) } fileprivate func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) { AssertIsOnMainThread() closeConversationBlock?() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in threadViewModel.threadRecord.softDelete(with: transaction) } - if let parent = parent as? ThreadSwipeHandlerUIUpdater { - parent.updateUIAfterSwipeAction() - } + updateUIAfterSwipeAction?() } fileprivate func markThreadAsRead(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in threadViewModel.threadRecord.markAllAsRead(updateStorageService: true, transaction: transaction) } } @@ -187,7 +176,7 @@ class ThreadSwipeHandler { fileprivate func markThreadAsUnread(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(isMarkedUnread: true, updateStorageService: true, transaction: transaction) } } @@ -209,13 +198,13 @@ class ThreadSwipeHandler { } alert.addAction(OWSActionSheets.cancelAction) - parent?.presentActionSheet(alert) + presentActionSheet(alert) } fileprivate func muteThread(threadViewModel: ThreadViewModel, duration seconds: TimeInterval) { AssertIsOnMainThread() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in let timeStamp = seconds < 0 ? ThreadAssociatedData.alwaysMutedTimestamp : (seconds == 0 ? 0 : Date.ows_millisecondTimestamp() + UInt64(seconds * 1000)) @@ -226,7 +215,7 @@ class ThreadSwipeHandler { fileprivate func unmuteThread(threadViewModel: ThreadViewModel) { AssertIsOnMainThread() - parent?.databaseStorage.write { transaction in + databaseStorage.write { transaction in threadViewModel.associatedData.updateWith(mutedUntilTimestamp: Date.ows_millisecondTimestamp(), updateStorageService: true, transaction: transaction) } } @@ -235,7 +224,7 @@ class ThreadSwipeHandler { AssertIsOnMainThread() do { - try parent?.databaseStorage.write { transaction in + try databaseStorage.write { transaction in try PinnedThreadManager.pinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) } } catch { @@ -252,7 +241,7 @@ class ThreadSwipeHandler { AssertIsOnMainThread() do { - try parent?.databaseStorage.write { transaction in + try databaseStorage.write { transaction in try PinnedThreadManager.unpinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction) } } catch { From 0712f943e9dd4a28c30782f42f72b71f5f28b4c9 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Thu, 9 Dec 2021 04:00:13 -0800 Subject: [PATCH 4/4] "Bump build to 5.27.0.39." (nightly-12-09-2021) --- Signal/Signal-Info.plist | 4 ++-- SignalNSE/Info.plist | 4 ++-- SignalShareExtension/Info.plist | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 99750a940e..47068c40a2 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 38 + 39 ITSAppUsesNonExemptEncryption LOGS_EMAIL @@ -141,7 +141,7 @@ INStartCallIntent OWSBundleVersion4 - 5.27.0.38 + 5.27.0.39 PHPhotoLibraryPreventAutomaticLimitedAccessAlert UIAppFonts diff --git a/SignalNSE/Info.plist b/SignalNSE/Info.plist index 610dce0c38..18f4fb0a7a 100644 --- a/SignalNSE/Info.plist +++ b/SignalNSE/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 5.27.0 CFBundleVersion - 38 + 39 NSAppTransportSecurity NSExceptionDomains @@ -55,7 +55,7 @@ $(PRODUCT_MODULE_NAME).NotificationService OWSBundleVersion4 - 5.27.0.38 + 5.27.0.39 UIAppFonts Inter-Medium.otf diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index b8015332c1..715e752b1d 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 5.27.0 CFBundleVersion - 38 + 39 ITSAppUsesNonExemptEncryption NSAppTransportSecurity @@ -77,7 +77,7 @@ com.apple.share-services OWSBundleVersion4 - 5.27.0.38 + 5.27.0.39 UIAppFonts fontawesome-webfont.ttf