From 5b5f4a4e88b52768b3112ab2d1958a1904c6c9d1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 May 2023 09:38:14 +1000 Subject: [PATCH] Various tweaks and fixes Fixed an issue where the GlobalSearch push animation could be jittery Fixed a crash which could occur when returning from the background on certain screens Removed the keyboard dismiss animation when pushing from global search to a conversation (apparently this is how iMessage avoids the animation bug...) Updated to the latest version of GRDB Updated the Atomic wrapper to use the ReadWrite lock for less blocking behaviours Updated the audio attachment icon to be consistent with Android & Desktop Updated the QuoteView to omit the "author" if we don't have their name and the quote can't be found --- Podfile.lock | 6 +- Session/Conversations/ConversationVC.swift | 21 +++-- .../Conversations/ConversationViewModel.swift | 78 ++++++++++++------ .../Conversations/Input View/InputView.swift | 6 -- .../Content Views/QuoteView.swift | 19 ++++- .../GlobalSearchViewController.swift | 37 ++++----- Session/Home/HomeVC.swift | 19 ++++- .../MessageRequestsViewController.swift | 5 +- .../DocumentTitleViewController.swift | 5 +- .../MediaInfoVC+MediaInfoView.swift | 2 + .../MediaInfoVC+MediaPreviewView.swift | 1 + .../Media Viewing & Editing/MediaInfoVC.swift | 2 + .../MediaPageViewController.swift | 5 +- .../MediaTileViewController.swift | 5 +- Session/Meta/AppDelegate.swift | 19 +++-- .../attachment_audio@1x.png | Bin 318 -> 369 bytes .../attachment_audio@2x.png | Bin 573 -> 628 bytes .../attachment_audio@3x.png | Bin 959 -> 893 bytes Session/Path/PathVC.swift | 18 +++- .../BlockedContactsViewController.swift | 5 +- .../Shared/SessionTableViewController.swift | 5 +- Session/Utilities/IP2Country.swift | 44 ++++++---- .../Database/Models/Interaction.swift | 4 + .../SessionThreadViewModel.swift | 13 ++- SessionShareExtension/ThreadPickerVC.swift | 5 +- SessionUtilitiesKit/General/Atomic.swift | 71 +++++++++++++--- 26 files changed, 275 insertions(+), 120 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 3088b746b..01df7da9c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,7 +27,7 @@ PODS: - DifferenceKit/Core (1.3.0) - DifferenceKit/UIKitExtension (1.3.0): - DifferenceKit/Core - - GRDB.swift/SQLCipher (6.10.1): + - GRDB.swift/SQLCipher (6.13.0): - SQLCipher (>= 3.4.2) - libwebp (1.2.1): - libwebp/demux (= 1.2.1) @@ -222,7 +222,7 @@ SPEC CHECKSUMS: CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca - GRDB.swift: 1cc67278f1a9878d6eb1b849485518112b79cab7 + GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78 libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84 NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 @@ -242,6 +242,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: e9443a8235dbff1fc342aa9bf08bbc66923adf68 +PODFILE CHECKSUM: f2f07345491c3a64dd6a526e87381a0e46a231d2 COCOAPODS: 1.11.3 diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 10c6922b6..d5ec2f3cc 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -169,7 +169,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl }() lazy var snInputView: InputView = InputView( - threadVariant: self.viewModel.threadData.threadVariant, + threadVariant: self.viewModel.initialThreadVariant, delegate: self ) @@ -180,6 +180,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl result.layer.cornerRadius = (ConversationVC.unreadCountViewSize / 2) result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize) result.set(.height, to: ConversationVC.unreadCountViewSize) + result.isHidden = true return result }() @@ -361,12 +362,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl scrollButton.pin(.right, to: .right, of: view, withInset: -20) messageRequestView.pin(.left, to: .left, of: view) messageRequestView.pin(.right, to: .right, of: view) - self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16) - self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16) - self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint - self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16) - self.scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16) - + messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16) + scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16) + scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint + scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16) + scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16) + messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10) messageRequestBlockButton.center(.horizontal, in: messageRequestView) @@ -483,7 +484,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges(didReturnFromBackground: true) + } + recoverInputView() if !isShowingSearchUI { diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index a5c30bed7..65f4ecec4 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -53,27 +53,65 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { // MARK: - Initialization init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionId: Int64?) { - // If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest - // unread interaction and start focused around that one - let targetInteractionId: Int64? = { - if let focusedInteractionId: Int64 = focusedInteractionId { return focusedInteractionId } + typealias InitialData = ( + targetInteractionId: Int64?, + currentUserIsClosedGroupMember: Bool?, + openGroupPermissions: OpenGroup.Permissions?, + blindedKey: String? + ) + + let initialData: InitialData? = Storage.shared.read { db -> InitialData in + let interaction: TypedTableAlias = TypedTableAlias() + let groupMember: TypedTableAlias = TypedTableAlias() - return Storage.shared.read { db in - let interaction: TypedTableAlias = TypedTableAlias() - - return try Interaction + // If we have a specified 'focusedInteractionId' then use that, otherwise retrieve the oldest + // unread interaction and start focused around that one + let targetInteractionId: Int64? = (focusedInteractionId != nil ? focusedInteractionId : + try Interaction .select(.id) .filter(interaction[.wasRead] == false) .filter(interaction[.threadId] == threadId) .order(interaction[.timestampMs].asc) .asRequest(of: Int64.self) .fetchOne(db) - } - }() + ) + let currentUserIsClosedGroupMember: Bool? = (threadVariant != .closedGroup ? nil : + try GroupMember + .filter(groupMember[.groupId] == threadId) + .filter(groupMember[.profileId] == getUserHexEncodedPublicKey(db)) + .filter(groupMember[.role] == GroupMember.Role.standard) + .isNotEmpty(db) + ) + let openGroupPermissions: OpenGroup.Permissions? = (threadVariant != .openGroup ? nil : + try OpenGroup + .filter(id: threadId) + .select(.permissions) + .asRequest(of: OpenGroup.Permissions.self) + .fetchOne(db) + ) + let blindedKey: String? = SessionThread.getUserHexEncodedBlindedKey( + db, + threadId: threadId, + threadVariant: threadVariant + ) + + return ( + targetInteractionId, + currentUserIsClosedGroupMember, + openGroupPermissions, + blindedKey + ) + } self.threadId = threadId self.initialThreadVariant = threadVariant - self.focusedInteractionId = targetInteractionId + self.focusedInteractionId = initialData?.targetInteractionId + self.threadData = SessionThreadViewModel( + threadId: threadId, + threadVariant: threadVariant, + currentUserIsClosedGroupMember: initialData?.currentUserIsClosedGroupMember, + openGroupPermissions: initialData?.openGroupPermissions + ).populatingCurrentUserBlindedKey(currentUserBlindedPublicKeyForThisThread: initialData?.blindedKey) self.pagedDataObserver = nil // Note: Since this references self we need to finish initializing before setting it, we @@ -93,7 +131,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { DispatchQueue.global(qos: .userInitiated).async { [weak self] in // If we don't have a `initialFocusedId` then default to `.pageBefore` (it'll query // from a `0` offset) - guard let initialFocusedId: Int64 = targetInteractionId else { + guard let initialFocusedId: Int64 = initialData?.targetInteractionId else { self?.pagedDataObserver?.load(.pageBefore) return } @@ -105,21 +143,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { // MARK: - Thread Data /// This value is the current state of the view - public private(set) lazy var threadData: SessionThreadViewModel = SessionThreadViewModel( - threadId: self.threadId, - threadVariant: self.initialThreadVariant, - currentUserIsClosedGroupMember: (self.initialThreadVariant != .closedGroup ? - nil : - Storage.shared.read { db in - try GroupMember - .filter(GroupMember.Columns.groupId == self.threadId) - .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) - .filter(GroupMember.Columns.role == GroupMember.Role.standard) - .isNotEmpty(db) - } - ) - ) - .populatingCurrentUserBlindedKey() + public private(set) var threadData: SessionThreadViewModel /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise /// performance https://github.com/groue/GRDB.swift#valueobservation-performance diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 8101060d1..4727b53fa 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -37,8 +37,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M set { inputTextView.selectedRange = newValue } } - var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder } - var enabledMessageTypes: MessageInputTypes = .all { didSet { setEnabledMessageTypes(enabledMessageTypes, message: nil) @@ -440,10 +438,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M override func resignFirstResponder() -> Bool { inputTextView.resignFirstResponder() } - - func inputTextViewBecomeFirstResponder() { - inputTextView.becomeFirstResponder() - } func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { // Not relevant in this case diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index ebd726a1c..69e90a357 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit final class QuoteView: UIView { static let thumbnailSize: CGFloat = 48 @@ -237,17 +238,27 @@ final class QuoteView: UIView { .compactMap { $0 } .asSet() .contains(authorId) + let authorLabel = UILabel() authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) - authorLabel.text = (isCurrentUser ? - "MEDIA_GALLERY_SENDER_NAME_YOU".localized() : - Profile.displayName( + authorLabel.text = { + guard !isCurrentUser else { return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() } + guard body != nil else { + // When we can't find the quoted message we want to hide the author label + return Profile.displayNameNoFallback( + id: authorId, + threadVariant: threadVariant + ) + } + + return Profile.displayName( id: authorId, threadVariant: threadVariant ) - ) + }() authorLabel.themeTextColor = targetThemeColor authorLabel.lineBreakMode = .byTruncatingTail + authorLabel.isHidden = (authorLabel.text == nil) let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace) authorLabel.set(.height, to: authorLabelSize.height) diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 139b5c383..d6e07e269 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -91,14 +91,17 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo setupNavigationBar() } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) searchBar.becomeFirstResponder() } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - searchBar.resignFirstResponder() + + UIView.performWithoutAnimation { + searchBar.resignFirstResponder() + } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -138,10 +141,6 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo } } - private func reloadTableData() { - tableView.reloadData() - } - // MARK: - Update Search Results private func refreshSearchResults() { @@ -155,9 +154,11 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo let searchText = rawSearchText.stripped guard searchText.count > 0 else { + guard searchText != (lastSearchText ?? "") else { return } + searchResultSet = defaultSearchResults lastSearchText = nil - reloadTableData() + tableView.reloadData() return } guard lastSearchText != searchText else { return } @@ -212,7 +213,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo .compactMap { $0 } .flatMap { $0 } self?.isLoading = false - self?.reloadTableData() + self?.tableView.reloadData() self?.refreshTimer = nil default: break @@ -283,18 +284,12 @@ extension GlobalSearchViewController { return } - if let presentedVC = self.presentedViewController { - presentedVC.dismiss(animated: false, completion: nil) - } - - let viewControllers: [UIViewController] = (self.navigationController? - .viewControllers) - .defaulting(to: []) - .appending( - ConversationVC(threadId: threadId, threadVariant: threadVariant, focusedInteractionId: focusedInteractionId) - ) - - self.navigationController?.setViewControllers(viewControllers, animated: true) + let viewController: ConversationVC = ConversationVC( + threadId: threadId, + threadVariant: threadVariant, + focusedInteractionId: focusedInteractionId + ) + self.navigationController?.pushViewController(viewController, animated: true) } // MARK: - UITableViewDataSource diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 1b15da4bb..5c89d8166 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -308,7 +308,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges(didReturnFromBackground: true) + } } @objc func applicationDidResignActive(_ notification: Notification) { @@ -393,8 +396,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi // in from a frame of CGRect.zero) guard hasLoadedInitialThreadData else { hasLoadedInitialThreadData = true - UIView.performWithoutAnimation { - handleThreadUpdates(updatedData, changeset: changeset, initialLoad: true) + + UIView.performWithoutAnimation { [weak self] in + // Hide the 'loading conversations' label (now that we have received conversation data) + self?.loadingConversationsLabel.isHidden = true + + // Show the empty state if there is no data + self?.emptyStateView.isHidden = ( + !updatedData.isEmpty && + updatedData.contains(where: { !$0.elements.isEmpty }) + ) + + self?.viewModel.updateThreadData(updatedData) } return } diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 2dfbbf89f..180b47761 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -162,7 +162,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges(didReturnFromBackground: true) + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift index 7a1e34733..bff7c7597 100644 --- a/Session/Media Viewing & Editing/DocumentTitleViewController.swift +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -119,7 +119,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges() + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges() + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift b/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift index 25c0f5774..b1c5483e1 100644 --- a/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift +++ b/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift @@ -3,6 +3,8 @@ import UIKit import SessionUIKit import SessionUtilitiesKit +import SessionMessagingKit +import SignalUtilitiesKit extension MediaInfoVC { final class MediaInfoView: UIView { diff --git a/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift b/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift index 2ca703e6e..462e143ff 100644 --- a/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift +++ b/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionUtilitiesKit +import SessionMessagingKit extension MediaInfoVC { final class MediaPreviewView: UIView { diff --git a/Session/Media Viewing & Editing/MediaInfoVC.swift b/Session/Media Viewing & Editing/MediaInfoVC.swift index 26711c83b..1d8991d4c 100644 --- a/Session/Media Viewing & Editing/MediaInfoVC.swift +++ b/Session/Media Viewing & Editing/MediaInfoVC.swift @@ -2,7 +2,9 @@ import UIKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit +import SignalUtilitiesKit final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate { internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 0ffc3cd42..7da69bfb9 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -245,7 +245,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges() + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges() + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index bbd5506c4..db7915b95 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -175,7 +175,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges(didReturnFromBackground: true) + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index d91851597..13046ce60 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -446,19 +446,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD guard CurrentAppContext().isMainApp else { return } - CurrentAppContext().setMainAppBadgeNumber( - Storage.shared + /// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database + /// read pools (up to a few seconds), since this read is blocking we want to dispatch it to run async to ensure + /// we don't block user interaction while it's running + DispatchQueue.global(qos: .default).async { + let unreadCount: Int = Storage.shared .read { db in let userPublicKey: String = getUserHexEncodedPublicKey(db) let thread: TypedTableAlias = TypedTableAlias() return try Interaction .filter(Interaction.Columns.wasRead == false) - .filter( - // Exclude outgoing and deleted messages from the count - Interaction.Columns.variant != Interaction.Variant.standardOutgoing && - Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted - ) + .filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant)) .filter( // Only count mentions if 'onlyNotifyForMentions' is set thread[.onlyNotifyForMentions] == false || @@ -482,7 +481,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD .fetchCount(db) } .defaulting(to: 0) - ) + + DispatchQueue.main.async { + CurrentAppContext().setMainAppBadgeNumber(unreadCount) + } + } } } diff --git a/Session/Meta/Images.xcassets/attachment_audio.imageset/attachment_audio@1x.png b/Session/Meta/Images.xcassets/attachment_audio.imageset/attachment_audio@1x.png index 53c019a7656506366f30be8a18e2457cc499e1c1..03d20502b119bae4ce142e3343052af3f85506ba 100644 GIT binary patch delta 342 zcmV-c0jd7J0`UTnB!3BTNLh0L01FcU01FcV0GgZ_00001b5ch_0Itp)=>Px#1ZP1_ zK>z@;j|==^1poj621!IgRCodHl1ih@_>hNCg72B7faTIVmHR(nbh?lAmj5 zJS80vfEYGgmq?6zS24#higfB2zmqPoQ zZeR^6riy5&Sb&<22`Hd~d&H4kIF5fH1v%-2`FG#nI?#7+U`!16FXdeaf2Gf>p|{iO z<#>p%K+azL33z#Yc{~Iyi)mJ^MPivHeAJrA5^`NC8L(*Wc7HCXecDO}WbW^l#yx<4 zODS5tPx#1ZP1_ zK>z@;j|==^1poj72}wjjRCodHnTu`0Fc60Sc7lxH4Vn#_5xPOM0c=1vARE92RVN4= zKqn|n02{PE#g>oAp>dpGqxzFh7JlWm4*!iy)&Lm}l=N`EdTQ+Wp|pHg;+7*h5_ z%FhjHqzFWakn)XI$+MJN${X6*Alb5VN^?YpR&I~}5vF`SLsUOQB$O~pi2?N+Reys> z(4$`ttS?}eTJTxOnKCZuS$Km7&+vyIlP-LzRKSBN3>eErrCv)YqydwV3Lz1cf_d^?uT|xr(kGt@q(u+qQ&IX)RDp=lu<+&)3 zciQuQj~Ixn0f)}V2^zs2TIH7*Mu}G0Bd(o%-R}uAQNVa!w)`{x_H_mN9iRqoj?pO2 z>LW!bzlJfQ>}gAFxbMsnFp&#^8L42xBVB5y1Sz^!^?%neqpRC?_sa%%@{==|QCwNd zOIF?$nw`qhX2Hq@?mz*f)ccJ(w5cnV(r?=-m9xHv9s-lFWFKB(9Av!SQxyVGVO+?N z>hTa{g@904gs*urq{MY#mr|kf`DUikzw5H;L!ceSzRhdf%yeRT2eqaSuaiPLpi6HZ zq4F*;H&iyogSNPS14T=Tna}p*k%cB15SoWKV5(!3j?RT8U%)snM@u>VYgr=#_%qr? p8hA40MOMHaa#0`RLQ5oKf=(NKRb!^?I?3mN(1%TxAh8e0Q1aG}O@DQRU}3 zzAd@fD?ZiWPMO3eZgy%9qEx9N0ZsZIsm9At%n}L$mC-z3pz)Pt5j;p@4xu#Lo``Fu=ZyP1RAY}%g z0`t^GcyM~G>VJLqf|GH_awHAlF|Z%GDA!JpRlSdK8Fws4lHG~CxanLv@7$MqxZs3) z<=9t*H_5Byz(t!$Px-6@2V8K%y%t<33KhHRMPm@1#2yR!SNgE=fCD=?*9JE5vxLBB zy{IfFKAG^PV(il*zHq?h4+Z^PJ?0AW9;82rnCSSJKYv_BaH^;zK^0jFeoyG%I<=;8F0lYqP7_*Qg#Px#1ZP1_ zK>z@;j|==^1poj85=lfsRCodHn}LzqFc3w5GAV!#PzSjZ)IqL;xPy=i&Z*$814soo zslby8paVz+Tq?MD2Rp2VX#uCV|@(#nKRNI(S=P=N$gAORIfKm`&|fdo__0ToC<1rkt!1XLga6-XeKKm`uFd<&#QoqM}_ zn=FpopGCkY)_!zNhW0Jo8Jfw+-o%KY9V^c|_>4icjeq>uV^FAJd>8Qn!{K%O@mx3X z|L?Pj6+v|)4E!SfW@U@vd|~B{=Q?jg5?j1H<7qwDE#l>}GN&#dJk9ii1nAPBg84k7 zA8uwflw;#QN#yV?(yP?tcl5IubJYpR~b2ME5XN9;n|qeijKqH^^^f(c-s4_?TT$**4&lBh1pND>-sB z$0F0>o8kO3ingV`Z;pSA_pRu-v& zKNhEhJNs2kJ4n5M%wA#o*B8z6P$-ihNhu-|DkDjsv&x ze1}%)Kaj{`G!_Qom&C4CpUh8wrW%4l`}>bilvi}`l9PkFQT?+hKA6*I2Fia{kXP%Y zqF;G20ToC<1rkt!1XLga6-Yn@68MC1p%=ooH{m%e8?^5v(pqeT3Bhw(v21|}5&9kc w5G!zD#_VF>5#H|T!&_CSq9}@@C`upx0njpi`KQN3fdBvi07*qoM6N<$f}L`ilmGw# delta 937 zcmV;a16KU~2EPZ8B!2;OQb$4nuFf3k000AdNkl2QU3WE4|q_eTMOX(kBX=Nad_y-7L6N|%uHiCskgb?HRb7o-NyUpb8y?1x- zZU%;(x3fF*-ESs)`|fsaEhQO929kmKF>q>rssGD-Q%|?0*ME+Bzp4Ew{e1MilFd1- zdPBM+J*+4`#xQoJxKFE+fasZ6OC_9hqD4>YCuZkzy7LVG5^xA?Te^rj zf^k*^*$H_BK!1adE@F#evF;?un5eW#P=y%lPJ)byN}B{#h_UWA3HoJ0)qj>>sC%Cq*2pYl`kbY2>O^f4 z^xdJj>TFCdM71qtOrO(ZX_U83f;`~zH!E(7wJl{#pVMP$l($WSb{&#C&c@^_-It=T z$s@7K>v?xwz^~43WVT7rmP7K`*=D{rqlI={tVb@c$Ktx@YhF_NeJO9$2huB2FWl)F zNGH9dP=Agm2C=-5i=2!F16USPwuQZJhOR}u@_lk%}#o|Dv76~;1$ zxt?%h_m&)C9}1&L?K?{;h<$DtyBDBS5*} zlYc%Fqc@*cRW^=saFE-U?n_Tf3S*fw^F!dgegV~5hwn=d)jp82`QDOVmLfl-pQO9e zE$Lh7Pw7g_ORe523VR`>&96HaVnJ<#NP+@Jlb|FhU?6a31tmcN1A#j$CK_ZDHK>JP8s+M8e((z@?(2%00000 LNkvXXu0mjf@8-U& diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 1b641f43f..6004e6ed9 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -218,9 +218,21 @@ final class PathVC: BaseVC { } private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView { - let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..." - let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "") - return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) + let country: String = (IP2Country.isInitialized ? + IP2Country.shared.countryNamesCache.wrappedValue[snode.ip].defaulting(to: "Resolving...") : + "Resolving..." + ) + + return getPathRow( + title: (isGuardSnode ? + "vc_path_guard_node_row_title".localized() : + "vc_path_service_node_row_title".localized() + ), + subtitle: country, + location: location, + dotAnimationStartDelay: dotAnimationStartDelay, + dotAnimationRepeatInterval: dotAnimationRepeatInterval + ) } // MARK: - Interaction diff --git a/Session/Settings/BlockedContactsViewController.swift b/Session/Settings/BlockedContactsViewController.swift index 7fbdb00af..cc6cfb85d 100644 --- a/Session/Settings/BlockedContactsViewController.swift +++ b/Session/Settings/BlockedContactsViewController.swift @@ -145,7 +145,10 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges(didReturnFromBackground: true) + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index 24b6cd82d..3d7def772 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -132,7 +132,10 @@ class SessionTableViewController = Atomic([:]) + private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue static var isInitialized = false // MARK: Tables - /// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains the **lower** bound of an IP - /// range and the "registered_country_geoname_id" column contains the ID of the country corresponding to that range. We look up an IP by finding the first index in the - /// network column where the value is greater than the IP we're looking up (converted to an integer). The IP we're looking up must then be in the range **before** that - /// range. + /// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains + /// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding + /// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking + /// up (converted to an integer). The IP we're looking up must then be in the range **before** that range. private lazy var ipv4Table: [String:[Int]] = { let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)! let data = try! Data(contentsOf: url) @@ -36,15 +37,23 @@ final class IP2Country { NotificationCenter.default.removeObserver(self) } - // MARK: Implementation - private func cacheCountry(for ip: String) -> String { - if let result = countryNamesCache[ip] { return result } - let ipAsInt = IPv4.toInt(ip) - guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted - let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex] - guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" } - let result = countryNamesTable["country_name"]![countryNamesTableIndex] - countryNamesCache[ip] = result + // MARK: - Implementation + + @discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String { + if let result: String = cache[ip] { return result } + + let ipAsInt: Int = IPv4.toInt(ip) + + guard + let ipv4TableIndex = ipv4Table["network"]?.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }), + let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex], + let countryNamesTableIndex = countryNamesTable["geoname_id"]?.firstIndex(of: String(countryID)), + let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex] + else { + return "Unknown Country" // Relies on the array being sorted + } + + cache[ip] = result return result } @@ -58,9 +67,12 @@ final class IP2Country { func populateCacheIfNeeded() -> Bool { guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false } - pathToDisplay.forEach { snode in - let _ = self.cacheCountry(for: snode.ip) // Preload if needed + countryNamesCache.mutate { [weak self] cache in + pathToDisplay.forEach { snode in + self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed + } } + DispatchQueue.main.async { IP2Country.isInitialized = true NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index a26b052ac..eb5b20be1 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -87,6 +87,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu // MARK: - Convenience + public static let variantsToIncrementUnreadCount: [Variant] = [ + .standardIncoming, .infoCall + ] + public var isInfoMessage: Bool { switch self { case .infoClosedGroupCreated, .infoClosedGroupUpdated, diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index b02681de6..793c07aaf 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -100,8 +100,14 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public var canWrite: Bool { switch threadVariant { case .contact: return true - case .closedGroup: return (currentUserIsClosedGroupMember == true) && (interactionVariant?.isGroupLeavingStatus != true) - case .openGroup: return openGroupPermissions?.contains(.write) ?? false + case .closedGroup: + return ( + currentUserIsClosedGroupMember == true && + interactionVariant?.isGroupLeavingStatus != true + ) + + case .openGroup: + return (openGroupPermissions?.contains(.write) ?? false) } } @@ -241,6 +247,7 @@ public extension SessionThreadViewModel { threadIsNoteToSelf: Bool = false, contactProfile: Profile? = nil, currentUserIsClosedGroupMember: Bool? = nil, + openGroupPermissions: OpenGroup.Permissions? = nil, unreadCount: UInt = 0 ) { self.rowId = -1 @@ -279,7 +286,7 @@ public extension SessionThreadViewModel { self.openGroupPublicKey = nil self.openGroupProfilePictureData = nil self.openGroupUserCount = nil - self.openGroupPermissions = nil + self.openGroupPermissions = openGroupPermissions // Interaction display info diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 5fb7aaf2a..77fa71796 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -85,7 +85,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges() + /// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query + DispatchQueue.main.async { [weak self] in + self?.startObservingChanges() + } } @objc func applicationDidResignActive(_ notification: Notification) { diff --git a/SessionUtilitiesKit/General/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift index 865745e14..d16ac102c 100644 --- a/SessionUtilitiesKit/General/Atomic.swift +++ b/SessionUtilitiesKit/General/Atomic.swift @@ -1,4 +1,4 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation @@ -6,23 +6,28 @@ import Foundation /// The `Atomic` wrapper is a generic wrapper providing a thread-safe way to get and set a value /// -/// A write-up on the need for this class and it's approach can be found here: +/// A write-up on the need for this class and it's approaches can be found at these links: +/// https://www.vadimbulavin.com/atomic-properties/ /// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/ /// there is also another approach which can be taken but it requires separate types for collections and results in /// a somewhat inconsistent interface between different `Atomic` wrappers +/// +/// We use a Read-write lock approach because the `DispatchQueue` approach means mutating the property +/// occurs on a different thread, and GRDB requires it's changes to be executed on specific threads so using a lock +/// is more compatible (and Read-write locks allow for concurrent reads which shouldn't be a huge issue but could +/// help reduce cases of blocking) @propertyWrapper public class Atomic { - // Note: Using 'userInteractive' to ensure this can't be blockedby higher priority queues - // which could result in the main thread getting blocked - private let queue: DispatchQueue = DispatchQueue( - label: "io.oxen.\(UUID().uuidString)", - qos: .userInteractive - ) private var value: Value + private let lock: ReadWriteLock = ReadWriteLock() /// In order to change the value you **must** use the `mutate` function public var wrappedValue: Value { - return queue.sync { return value } + lock.readLock() + let result: Value = value + lock.unlock() + + return result } /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections @@ -36,12 +41,34 @@ public class Atomic { self.value = initialValue } + public init(wrappedValue: Value) { + self.value = wrappedValue + } + // MARK: - Functions @discardableResult public func mutate(_ mutation: (inout Value) -> T) -> T { - return queue.sync { - return mutation(&value) + lock.writeLock() + let result: T = mutation(&value) + lock.unlock() + + return result + } + + @discardableResult public func mutate(_ mutation: (inout Value) throws -> T) throws -> T { + let result: T + + do { + lock.writeLock() + result = try mutation(&value) + lock.unlock() } + catch { + lock.unlock() + throw error + } + + return result } } @@ -50,3 +77,25 @@ extension Atomic where Value: CustomDebugStringConvertible { return value.debugDescription } } + +// MARK: - ReadWriteLock + +private class ReadWriteLock { + private var rwlock: pthread_rwlock_t = { + var rwlock = pthread_rwlock_t() + pthread_rwlock_init(&rwlock, nil) + return rwlock + }() + + func writeLock() { + pthread_rwlock_wrlock(&rwlock) + } + + func readLock() { + pthread_rwlock_rdlock(&rwlock) + } + + func unlock() { + pthread_rwlock_unlock(&rwlock) + } +}