diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index cbe87d1b0..a7b9fc76d 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -109,6 +109,9 @@ extension ContextMenuVC { delegate: ContextMenuActionDelegate? ) -> [Action]? { // No context items for info messages + guard cellViewModel.variant != .standardIncomingDeleted else { + return [ Action.delete(cellViewModel, delegate) ] + } guard cellViewModel.variant == .standardOutgoing || cellViewModel.variant == .standardIncoming else { return nil } diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 4f32de036..3f3146df2 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -221,7 +221,7 @@ final class ContextMenuVC: UIViewController { menuView.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) emojiBar.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) - case .standardIncoming: + case .standardIncoming, .standardIncomingDeleted: menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX) emojiBar.pin(.left, to: .left, of: view, withInset: targetFrame.minX) @@ -288,8 +288,8 @@ final class ContextMenuVC: UIViewController { let ratio: CGFloat = (frame.width / frame.height) // FIXME: Need to update this when an appropriate replacement is added (see https://teng.pub/technical/2021/11/9/uiapplication-key-window-replacement) - let topMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.top, Values.mediumSpacing) - let bottomMargin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) + let topMargin = max((UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0), Values.mediumSpacing) + let bottomMargin = max((UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0), Values.mediumSpacing) let diffY = finalFrame.height + menuHeight + Self.actionViewHeight + 2 * spacing + topMargin + bottomMargin - UIScreen.main.bounds.height if diffY > 0 { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index d9e90db04..8f6317be5 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1594,6 +1594,14 @@ extension ConversationVC: func delete(_ cellViewModel: MessageViewModel) { // Only allow deletion on incoming and outgoing messages + guard cellViewModel.variant != .standardIncomingDeleted else { + Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + return + } guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardOutgoing else { return } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 4ef01d631..9deefda38 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -138,11 +138,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl result.showsVerticalScrollIndicator = false result.contentInsetAdjustmentBehavior = .never result.keyboardDismissMode = .interactive - let bottomInset: CGFloat = viewModel.threadData.canWrite ? Values.mediumSpacing : Values.mediumSpacing + UIApplication.shared.keyWindow!.safeAreaInsets.bottom result.contentInset = UIEdgeInsets( top: 0, leading: 0, - bottom: bottomInset, + bottom: (viewModel.threadData.canWrite ? + Values.mediumSpacing : + (Values.mediumSpacing + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)) + ), trailing: 0 ) result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self) @@ -604,11 +606,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl snInputView.text = draft } - // Now we have done all the needed diffs, update the viewModel with the latest data and mark - // all messages as read (we do it in here as the 'threadData' actually contains the last - // 'interactionId' for the thread) + // Now we have done all the needed diffs update the viewModel with the latest data self.viewModel.updateThreadData(updatedThreadData) - self.viewModel.markAllAsRead() /// **Note:** This needs to happen **after** we have update the viewModel's thread data if initialLoad || viewModel.threadData.currentUserIsClosedGroupMember != updatedThreadData.currentUserIsClosedGroupMember { @@ -682,7 +681,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl source: viewModel.interactionData, target: updatedData ) - let isInsert: Bool = (changeset.map({ $0.elementInserted.count }).reduce(0, +) > 0) + let numItemsInserted: Int = changeset.map { $0.elementInserted.count }.reduce(0, +) + let isInsert: Bool = (numItemsInserted > 0) let wasLoadingMore: Bool = self.isLoadingMore let wasOffsetCloseToBottom: Bool = self.isCloseToBottom let numItemsInUpdatedData: [Int] = updatedData.map { $0.elements.count } @@ -758,10 +758,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } } } - else if wasOffsetCloseToBottom && !wasLoadingMore { - // Scroll to the bottom if an interaction was just inserted and we either - // just sent a message or are close enough to the bottom (wait a tiny fraction - // to avoid buggy animation behaviour) + else if wasOffsetCloseToBottom && !wasLoadingMore && numItemsInserted < 5 { + /// Scroll to the bottom if an interaction was just inserted and we either just sent a message or are close enough to the + /// bottom (wait a tiny fraction to avoid buggy animation behaviour) + /// + /// **Note:** We won't automatically scroll to the bottom if 5 or more messages were inserted (to avoid endlessly + /// auto-scrolling to the bottom when fetching new pages of data within open groups DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in self?.scrollToBottom(isAnimated: true) } @@ -771,6 +773,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl self.isLoadingMore = false self.autoLoadNextPageIfNeeded() } + else { + // Need to update the scroll button alpha in case new messages were added but we didn't scroll + self.scrollButton.alpha = self.getScrollButtonOpacity() + self.unreadCountView.alpha = self.scrollButton.alpha + } return } @@ -1311,6 +1318,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl ) self.handleInitialOffsetBounceBug(targetIndexPath: targetIndexPath, at: .bottom) + self.viewModel.markAsRead(beforeInclusive: nil) } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -1322,8 +1330,41 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollButton.alpha = getScrollButtonOpacity() - unreadCountView.alpha = scrollButton.alpha + self.scrollButton.alpha = self.getScrollButtonOpacity() + self.unreadCountView.alpha = self.scrollButton.alpha + + // We want to mark messages as read while we scroll, so grab the newest message and mark + // everything older as read + // + // Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance + // the table content appears above the input view + let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing)) + + if + let visibleIndexPaths: [IndexPath] = self.tableView.indexPathsForVisibleRows, + let messagesSection: Int = visibleIndexPaths + .first(where: { self.viewModel.interactionData[$0.section].model == .messages })? + .section, + let newestCellViewModel: MessageViewModel = visibleIndexPaths + .sorted() + .filter({ $0.section == messagesSection }) + .compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in + guard let frame: CGRect = tableView.cellForRow(at: indexPath)?.frame else { + return nil + } + + return ( + view.convert(frame, from: tableView), + self.viewModel.interactionData[indexPath.section].elements[indexPath.row] + ) + }) + // Exclude messages that are partially off the bottom of the screen + .filter({ $0.frame.maxY <= tableVisualBottom }) + .last? + .cellViewModel + { + self.viewModel.markAsRead(beforeInclusive: newestCellViewModel.id) + } } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { @@ -1472,6 +1513,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // Store the info incase we need to load more data (call will be re-triggered) self.focusedInteractionId = interactionId self.shouldHighlightNextScrollToInteraction = highlight + self.viewModel.markAsRead(beforeInclusive: interactionId) // Ensure the target interaction has been loaded guard diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 2be7fd079..3cd2d9388 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -149,6 +149,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { // MARK: - Interaction Data + private var lastInteractionIdMarkedAsRead: Int64? public private(set) var unobservedInteractionDataChanges: [SectionModel]? public private(set) var interactionData: [SectionModel] = [] public private(set) var reactionExpandedInteractionIds: Set = [] @@ -380,21 +381,30 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { } } - public func markAllAsRead() { - // Don't bother marking anything as read if there are no unread interactions (we can rely - // on the 'threadData.threadUnreadCount' to always be accurate) + /// This method will mark all interactions as read before the specified interaction id, if no id is provided then all interactions for + /// the thread will be marked as read + public func markAsRead(beforeInclusive interactionId: Int64?) { + /// Since this method now gets triggered when scrolling we want to try to optimise it and avoid busying the database + /// write queue when it isn't needed, in order to do this we: + /// + /// - Don't bother marking anything as read if there are no unread interactions (we can rely on the + /// `threadData.threadUnreadCount` to always be accurate) + /// - Don't bother marking anything as read if this was called with the same `interactionId` that we + /// previously marked as read (ie. when scrolling and the last message hasn't changed) guard (self.threadData.threadUnreadCount ?? 0) > 0, - let lastInteractionId: Int64 = self.threadData.interactionId + let targetInteractionId: Int64 = (interactionId ?? self.threadData.interactionId), + self.lastInteractionIdMarkedAsRead != targetInteractionId else { return } let threadId: String = self.threadData.threadId let trySendReadReceipt: Bool = (self.threadData.threadIsMessageRequest == false) + self.lastInteractionIdMarkedAsRead = targetInteractionId Storage.shared.writeAsync { db in try Interaction.markAsRead( db, - interactionId: lastInteractionId, + interactionId: targetInteractionId, threadId: threadId, includingOlder: true, trySendReadReceipt: trySendReadReceipt diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index b37f2e1c1..caa9722c3 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -1049,51 +1049,52 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) // Custom handle links - let links: [String: NSRange] = { - guard - let body: String = cellViewModel.body, - let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - else { return [:] } - - var links: [String: NSRange] = [:] - let matches = detector.matches( - in: body, - options: [], - range: NSRange(location: 0, length: body.count) - ) + let links: [URL: NSRange] = { + guard let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return [:] + } - for match in matches { - guard let matchURL = match.url else { continue } - - /// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and - /// set the scheme to 'https' instead as we don't load previews for 'http' so this will result - /// in more previews actually getting loaded without forcing the user to enter 'https://' before - /// every URL they enter - let urlString: String = (matchURL.absoluteString == "http://\(body)" ? - "https://\(body)" : - matchURL.absoluteString + return detector + .matches( + in: attributedText.string, + options: [], + range: NSRange(location: 0, length: attributedText.string.count) ) - - if URL(string: urlString) != nil { - links[urlString] = (body as NSString).range(of: urlString) + .reduce(into: [:]) { result, match in + guard + let matchUrl: URL = match.url, + let originalRange: Range = Range(match.range, in: attributedText.string) + else { return } + + /// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and + /// set the scheme to 'https' instead as we don't load previews for 'http' so this will result + /// in more previews actually getting loaded without forcing the user to enter 'https://' before + /// every URL they enter + let originalString: String = String(attributedText.string[originalRange]) + + guard matchUrl.absoluteString != "http://\(originalString)" else { + guard let httpsUrl: URL = URL(string: "https://\(originalString)") else { + return + } + + result[httpsUrl] = match.range + return + } + + result[matchUrl] = match.range } - } - - return links }() - for (urlString, range) in links { - guard let url: URL = URL(string: urlString) else { continue } - + for (linkUrl, urlRange) in links { attributedText.addAttributes( [ .font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)), .foregroundColor: actualTextColor, .underlineColor: actualTextColor, .underlineStyle: NSUnderlineStyle.single.rawValue, - .attachment: url + .attachment: linkUrl ], - range: range + range: urlRange ) } @@ -1105,7 +1106,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { .map { part -> String in guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part } - return String(part[part.index(after: part.startIndex).. = { + let term: String = String(normalizedBody[range]) + + // If the matched term doesn't actually match the "part" value then it means + // we've matched a term after a non-alphanumeric character so need to shift + // the range over by 1 + guard term.starts(with: part.lowercased()) else { + return (normalizedBody.index(after: range.lowerBound).. String in guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part } - return String(part[part.index(after: part.startIndex).. = { + let term: String = String(normalizedSnippet[range]) + + // If the matched term doesn't actually match the "part" value then it means + // we've matched a term after a non-alphanumeric character so need to shift + // the range over by 1 + guard term.starts(with: part.lowercased()) else { + return (normalizedSnippet.index(after: range.lowerBound).. Int64 { + return (value > UInt64(Int64.max) ? 0 : Int64(value)) + } +} diff --git a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift index c9e8b8af3..bf27ef218 100644 --- a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift @@ -109,6 +109,7 @@ public extension SendReadReceiptsJob { .filter(interactionIds.contains(Interaction.Columns.id)) // Only `standardIncoming` incoming interactions should have read receipts sent .filter(Interaction.Columns.variant == Interaction.Variant.standardIncoming) + .filter(Interaction.Columns.wasRead == false) // Only send for unread messages .joining( // Don't send read receipts in group threads required: Interaction.thread @@ -119,7 +120,10 @@ public extension SendReadReceiptsJob { ) // If there are no timestamp values then do nothing - guard let timestampMsValues: [Int64] = maybeTimestampMsValues else { return nil } + guard + let timestampMsValues: [Int64] = maybeTimestampMsValues, + !timestampMsValues.isEmpty + else { return nil } // Try to get an existing job (if there is one that's not running) if diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 88e478424..b311fa9d9 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -940,6 +940,13 @@ public extension SessionThreadViewModel { public extension SessionThreadViewModel { static let searchResultsLimit: Int = 500 + /// FTS will fail or try to process characters outside of `[A-Za-z0-9]` are included directly in a search + /// term, in order to resolve this the term needs to be wrapped in quotation marks so the eventual SQL + /// is `MATCH '"{term}"'` or `MATCH '"{term}"*'` + static func searchSafeTerm(_ term: String) -> String { + return "\"\(term)\"" + } + static func searchTermParts(_ searchTerm: String) -> [String] { /// Process the search term in order to extract the parts of the search pattern we want /// @@ -954,7 +961,7 @@ public extension SessionThreadViewModel { guard index % 2 == 1 else { return String(value) .split(separator: " ") - .map { String($0) } + .map { "\"\(String($0))\"" } } return ["\"\(value)\""] @@ -972,13 +979,14 @@ public extension SessionThreadViewModel { let rawPattern: String = searchTermParts(searchTerm) .joined(separator: " OR ") .appending("*") + let fallbackTerm: String = "\(searchSafeTerm(searchTerm))*" /// There are cases where creating a pattern can fail, we want to try and recover from those cases /// by failling back to simpler patterns if needed let maybePattern: FTS5Pattern? = (try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: table)) .defaulting( - to: (try? db.makeFTS5Pattern(rawPattern: searchTerm, forTable: table)) - .defaulting(to: FTS5Pattern(matchingAnyTokenIn: searchTerm)) + to: (try? db.makeFTS5Pattern(rawPattern: fallbackTerm, forTable: table)) + .defaulting(to: FTS5Pattern(matchingAnyTokenIn: fallbackTerm)) ) guard let pattern: FTS5Pattern = maybePattern else { throw StorageError.invalidSearchPattern } diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 81408b1bd..14b9b9f61 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -15,12 +15,6 @@ public extension Setting.EnumKey { } public extension Setting.BoolKey { - /// Controls whether the preview screen in the app switcher should be enabled - /// - /// **Note:** In the legacy setting this flag controlled whether the preview was "disabled" (and defaulted to - /// true), by inverting this flag we can default it to false as is standard for Bool values - static let appSwitcherPreviewEnabled: Setting.BoolKey = "appSwitcherPreviewEnabled" - /// Controls whether typing indicators are enabled /// /// **Note:** Only works if both participants in a "contact" thread have this setting enabled diff --git a/SessionUIKit/Components/Modal.swift b/SessionUIKit/Components/Modal.swift index 436f04242..25c77d1ec 100644 --- a/SessionUIKit/Components/Modal.swift +++ b/SessionUIKit/Components/Modal.swift @@ -116,8 +116,8 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { } public static func createButton(title: String, titleColor: ThemeValue) -> UIButton { - let result: UIButton = UIButton() // TODO: NEED to fix the font (looks bad) - result.titleLabel?.font = .systemFont(ofSize: Values.mediumFontSize, weight: UIFont.Weight(600)) + let result: UIButton = UIButton() + result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.setTitle(title, for: .normal) result.setThemeTitleColor(titleColor, for: .normal) result.setThemeBackgroundColor(.alert_buttonBackground, for: .normal) diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 56fde9e6c..ad4941d98 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -360,14 +360,26 @@ public final class Storage { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } guard let observer: TransactionObserver = observer else { return } - dbWriter.add(transactionObserver: observer) + // Note: This actually triggers a write to the database so can be blocked by other + // writes, since it's usually called on the main thread when creating a view controller + // this can result in the UI hanging - to avoid this we dispatch (and hope there isn't + // negative impact) + DispatchQueue.global(qos: .default).async { + dbWriter.add(transactionObserver: observer) + } } public func removeObserver(_ observer: TransactionObserver?) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } guard let observer: TransactionObserver = observer else { return } - dbWriter.remove(transactionObserver: observer) + // Note: This actually triggers a write to the database so can be blocked by other + // writes, since it's usually called on the main thread when creating a view controller + // this can result in the UI hanging - to avoid this we dispatch (and hope there isn't + // negative impact) + DispatchQueue.global(qos: .default).async { + dbWriter.remove(transactionObserver: observer) + } } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 0d2bafdec..e5e0362ad 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -5,10 +5,11 @@ import Foundation import AVFoundation import MediaPlayer +import CoreServices import PromiseKit import SessionUIKit -import CoreServices import SessionMessagingKit +import SignalCoreKit public protocol AttachmentApprovalViewControllerDelegate: AnyObject { func attachmentApproval( @@ -538,7 +539,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC private func setCurrentItem(_ item: SignalAttachmentItem?, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) { guard let item: SignalAttachmentItem = item, let page = self.buildPage(item: item) else { - owsFailDebug("unexpectedly unable to build new page") + Logger.error("unexpectedly unable to build new page") return } @@ -550,7 +551,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC func updateMediaRail() { guard let currentItem = self.currentItem else { - owsFailDebug("currentItem was unexpectedly nil") + Logger.error("currentItem was unexpectedly nil") return } @@ -565,7 +566,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return cell default: - owsFailDebug("unexpted rail item type: \(railItem)") + Logger.error("unexpted rail item type: \(railItem)") return GalleryRailCellView() } } diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift b/SignalUtilitiesKit/Screen Lock/ScreenLock.swift index ca9896bf2..a4158d358 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift +++ b/SignalUtilitiesKit/Screen Lock/ScreenLock.swift @@ -105,7 +105,7 @@ public class ScreenLock { defaultErrorDescription: defaultErrorDescription) switch outcome { case .success: - owsFailDebug("local authentication unexpected success") + Logger.error("local authentication unexpected success") completion(.failure(error: defaultErrorDescription)) case .cancel, .failure, .unexpectedFailure: @@ -129,8 +129,8 @@ public class ScreenLock { switch outcome { case .success: - owsFailDebug("local authentication unexpected success") - completion(.failure(error:defaultErrorDescription)) + Logger.error("local authentication unexpected success") + completion(.failure(error: defaultErrorDescription)) case .cancel, .failure, .unexpectedFailure: completion(outcome) @@ -190,11 +190,11 @@ public class ScreenLock { return .failure(error: "SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT".localized()) case .invalidContext: - owsFailDebug("context not valid.") + Logger.error("context not valid.") return .unexpectedFailure(error: defaultErrorDescription) case .notInteractive: - owsFailDebug("context not interactive.") + Logger.error("context not interactive.") return .unexpectedFailure(error: defaultErrorDescription) @unknown default: