Merge branch 'master' into martin/PAPERCUT-15

This commit is contained in:
Martin Böttcher 2021-12-10 08:06:25 +01:00
commit 24eae22fda
9 changed files with 337 additions and 264 deletions

View File

@ -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 */,

View File

@ -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>

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -405,3 +405,9 @@ extension HomeViewController: BadgeExpirationSheetDelegate {
func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) { }
}
extension HomeViewController: ThreadSwipeHandler {
func updateUIAfterSwipeAction() {
updateViewState()
}
}

View 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)")
}
}
}

View File

@ -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>

View File

@ -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>