Merge branch 'master' into martin/PAPERCUT-15
This commit is contained in:
commit
24eae22fda
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
1477630A275E20D700D1067E /* ThreadSwipeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSwipeHandler.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
1704690925D4C2E6000793D8 /* SignalAttachmentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAttachmentTest.swift; sourceTree = "<group>"; };
|
||||
1704690B25D4C92B000793D8 /* test-jpg-rotated.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "test-jpg-rotated.jpg"; sourceTree = "<group>"; };
|
||||
@ -2706,6 +2708,7 @@
|
||||
34E95C1D269F4F4F004807EC /* HVRenderState.swift */,
|
||||
34E95C20269F4F4F004807EC /* HVTableDataSource.swift */,
|
||||
34E95C1F269F4F4F004807EC /* HVViewState.swift */,
|
||||
1477630A275E20D700D1067E /* ThreadSwipeHandler.swift */,
|
||||
);
|
||||
path = HomeView;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>38</string>
|
||||
<string>39</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LOGS_EMAIL</key>
|
||||
@ -141,7 +141,7 @@
|
||||
<string>INStartCallIntent</string>
|
||||
</array>
|
||||
<key>OWSBundleVersion4</key>
|
||||
<string>5.27.0.38</string>
|
||||
<string>5.27.0.39</string>
|
||||
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
|
||||
<true/>
|
||||
<key>UIAppFonts</key>
|
||||
|
||||
@ -11,7 +11,7 @@ public protocol ConversationSearchViewDelegate: AnyObject {
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ConversationSearchViewController: UITableViewController {
|
||||
public class ConversationSearchViewController: UITableViewController, ThreadSwipeHandler {
|
||||
|
||||
// MARK: -
|
||||
|
||||
@ -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,16 @@ public class ConversationSearchViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public override func tableView(_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return trailingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath))
|
||||
}
|
||||
|
||||
public override func tableView(_ tableView: UITableView,
|
||||
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return leadingSwipeActionsConfiguration(for: getThreadViewModelFor(indexPath: indexPath))
|
||||
}
|
||||
|
||||
// MARK: Update Search Results
|
||||
|
||||
var refreshTimer: Timer?
|
||||
@ -514,7 +526,14 @@ public class ConversationSearchViewController: UITableViewController {
|
||||
reloadTableData()
|
||||
return
|
||||
}
|
||||
guard lastSearchText != searchText else {
|
||||
|
||||
// a database change will lead to a search with the searchText=lastSearchText
|
||||
// in this case we only want to update the visible cells
|
||||
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
|
||||
}
|
||||
@ -538,14 +557,48 @@ 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 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) {
|
||||
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)
|
||||
|
||||
|
||||
@ -665,72 +665,15 @@ 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 viewController.trailingSwipeActionsConfiguration(for: threadViewModel,
|
||||
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 ])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,57 +711,12 @@ 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)
|
||||
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)
|
||||
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 viewController.leadingSwipeActionsConfiguration(for: threadViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -405,3 +405,9 @@ extension HomeViewController: BadgeExpirationSheetDelegate {
|
||||
|
||||
func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) { }
|
||||
}
|
||||
|
||||
extension HomeViewController: ThreadSwipeHandler {
|
||||
func updateUIAfterSwipeAction() {
|
||||
updateViewState()
|
||||
}
|
||||
}
|
||||
|
||||
252
Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift
Normal file
252
Signal/src/ViewControllers/HomeView/ThreadSwipeHandler.swift
Normal file
@ -0,0 +1,252 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalMessaging
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
protocol ThreadSwipeHandler {
|
||||
@objc optional func updateUIAfterSwipeAction()
|
||||
}
|
||||
|
||||
extension ThreadSwipeHandler where Self: UIViewController {
|
||||
|
||||
func leadingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?) -> 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)
|
||||
}
|
||||
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 self] (_, _, completion) in
|
||||
completion(false)
|
||||
self?.pinThread(threadViewModel: threadViewModel)
|
||||
}
|
||||
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) { [weak self] (_, _, completion) in
|
||||
completion(false)
|
||||
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) { [weak self] (_, _, completion) in
|
||||
completion(false)
|
||||
self?.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 ])
|
||||
}
|
||||
|
||||
func trailingSwipeActionsConfiguration(for threadViewModel: ThreadViewModel?, closeConversationBlock: (() -> Void)? = nil) -> UISwipeActionsConfiguration? {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let threadViewModel = threadViewModel else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let muteAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, 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) { [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) { [weak self] (_, _, completion) in
|
||||
self?.archiveThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock)
|
||||
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])
|
||||
}
|
||||
|
||||
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)?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
closeConversationBlock?()
|
||||
databaseStorage.write { transaction in
|
||||
threadViewModel.associatedData.updateWith(isArchived: !threadViewModel.isArchived,
|
||||
updateStorageService: true,
|
||||
transaction: transaction)
|
||||
}
|
||||
updateUIAfterSwipeAction?()
|
||||
}
|
||||
|
||||
fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> 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)
|
||||
})
|
||||
alert.addAction(OWSActionSheets.cancelAction)
|
||||
|
||||
presentActionSheet(alert)
|
||||
}
|
||||
|
||||
fileprivate func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
closeConversationBlock?()
|
||||
databaseStorage.write { transaction in
|
||||
threadViewModel.threadRecord.softDelete(with: transaction)
|
||||
}
|
||||
updateUIAfterSwipeAction?()
|
||||
}
|
||||
|
||||
fileprivate func markThreadAsRead(threadViewModel: ThreadViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
threadViewModel.threadRecord.markAllAsRead(updateStorageService: true, transaction: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func markThreadAsUnread(threadViewModel: ThreadViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
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)
|
||||
|
||||
presentActionSheet(alert)
|
||||
}
|
||||
|
||||
fileprivate 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)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func unmuteThread(threadViewModel: ThreadViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
threadViewModel.associatedData.updateWith(mutedUntilTimestamp: Date.ows_millisecondTimestamp(), updateStorageService: true, transaction: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func unpinThread(threadViewModel: ThreadViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
do {
|
||||
try databaseStorage.write { transaction in
|
||||
try PinnedThreadManager.unpinThread(threadViewModel.threadRecord, updateStorageService: true, transaction: transaction)
|
||||
}
|
||||
} catch {
|
||||
owsFailDebug("Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.27.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>38</string>
|
||||
<string>39</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
@ -55,7 +55,7 @@
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
<key>OWSBundleVersion4</key>
|
||||
<string>5.27.0.38</string>
|
||||
<string>5.27.0.39</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Inter-Medium.otf</string>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.27.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>38</string>
|
||||
<string>39</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
@ -77,7 +77,7 @@
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
<key>OWSBundleVersion4</key>
|
||||
<string>5.27.0.38</string>
|
||||
<string>5.27.0.39</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>fontawesome-webfont.ttf</string>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user