diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c3b855e33d..07afc3af66 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8547,7 +8547,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 665; + CURRENT_PROJECT_VERSION = 668; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8628,7 +8628,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 665; + CURRENT_PROJECT_VERSION = 668; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -9328,7 +9328,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 665; + CURRENT_PROJECT_VERSION = 668; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9917,7 +9917,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 665; + CURRENT_PROJECT_VERSION = 668; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index c27e705141..32ca57b8c6 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -35,12 +35,12 @@ extension ConversationVC: } // Handle taps outside of tableview cell to dismiss keyboard - @MainActor @objc func dismissKeyboardOnTap(_ recognizer: UITapGestureRecognizer) { - /// If the tap was inside the "Send" button on the input then we **don't** want to dismiss the keyboard (the user should be - /// able to send multiple messages in a row) - let location: CGPoint = recognizer.location(in: self.snInputView.sendButton) + @MainActor @objc func dismissKeyboardOnMessageListTap(_ recognizer: UITapGestureRecognizer) { + /// If the tap was inside the input then we **don't** want to dismiss the keyboard (the user should be able to interact with their + /// current text, or tap the buttons without the keyboard being dismissed) + let location: CGPoint = recognizer.location(in: self.snInputView) - guard !snInputView.sendButton.bounds.contains(location) else { return } + guard !snInputView.bounds.contains(location) else { return } _ = self.snInputView.resignFirstResponder() } @@ -1642,41 +1642,43 @@ extension ConversationVC: return cellViewModel.profile?.blocksCommunityMessageRequests != true }() - let userProfileModal: ModalHostingViewController = ModalHostingViewController( - modal: UserProfileModal( - info: .init( - sessionId: sessionId, - blindedId: blindedId, - qrCodeImage: qrCodeImage, - profileInfo: profileInfo, - displayName: displayName, - contactDisplayName: contactDisplayName, - isProUser: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: cellViewModel.profile) }), - isMessageRequestsEnabled: isMessasgeRequestsEnabled, - onStartThread: { [weak self] in - self?.startThread( - with: cellViewModel.authorId, - openGroupServer: cellViewModel.threadOpenGroupServer, - openGroupPublicKey: cellViewModel.threadOpenGroupPublicKey - ) - }, - onProBadgeTapped: { [weak self, dependencies] in - dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( - .generic, - dismissType: .single, - afterClosed: { [weak self] in - self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") - }, - presenting: { modal in - dependencies[singleton: .appContext].frontMostViewController?.present(modal, animated: true) - } - ) - } - ), - dataManager: dependencies[singleton: .imageDataManager] + DispatchQueue.main.async { [weak self] in + let userProfileModal: ModalHostingViewController = ModalHostingViewController( + modal: UserProfileModal( + info: .init( + sessionId: sessionId, + blindedId: blindedId, + qrCodeImage: qrCodeImage, + profileInfo: profileInfo, + displayName: displayName, + contactDisplayName: contactDisplayName, + isProUser: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: cellViewModel.profile) }), + isMessageRequestsEnabled: isMessasgeRequestsEnabled, + onStartThread: { [weak self] in + self?.startThread( + with: cellViewModel.authorId, + openGroupServer: cellViewModel.threadOpenGroupServer, + openGroupPublicKey: cellViewModel.threadOpenGroupPublicKey + ) + }, + onProBadgeTapped: { [weak self, dependencies] in + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + .generic, + dismissType: .single, + afterClosed: { [weak self] in + self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") + }, + presenting: { modal in + dependencies[singleton: .appContext].frontMostViewController?.present(modal, animated: true) + } + ) + } + ), + dataManager: dependencies[singleton: .imageDataManager] + ) ) - ) - present(userProfileModal, animated: true, completion: nil) + self?.present(userProfileModal, animated: true, completion: nil) + } } func startThread( diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 69f19adad4..2527da65d0 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -453,7 +453,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa private lazy var tableViewTapGesture: UITapGestureRecognizer = { let result: UITapGestureRecognizer = UITapGestureRecognizer() result.delegate = self - result.addTarget(self, action: #selector(dismissKeyboardOnTap)) + result.addTarget(self, action: #selector(dismissKeyboardOnMessageListTap)) result.cancelsTouchesInView = false return result diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 56cb7a763b..9fc9d0a3f0 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -2010,7 +2010,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi private func toggleConversationPinnedStatus(currentPinnedPriority: Int32) { let isCurrentlyPinned: Bool = (currentPinnedPriority > LibSession.visiblePriority) - if !isCurrentlyPinned && !dependencies[cache: .libSession].isSessionPro { + if !isCurrentlyPinned && dependencies[feature: .sessionProEnabled] && !dependencies[cache: .libSession].isSessionPro { // TODO: [Database Relocation] Retrieve the full conversation list from lib session and check the pinnedPriority that way instead of using the database dependencies[singleton: .storage].writeAsync ( updates: { [threadId, dependencies] db in @@ -2039,16 +2039,18 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi numPinnedConversations > 0 else { return } - let sessionProModal: ModalHostingViewController = ModalHostingViewController( - modal: ProCTAModal( - delegate: dependencies[singleton: .sessionProState], - variant: .morePinnedConvos( - isGrandfathered: (numPinnedConversations > LibSession.PinnedConversationLimit) - ), - dataManager: dependencies[singleton: .imageDataManager] + DispatchQueue.main.async { + let sessionProModal: ModalHostingViewController = ModalHostingViewController( + modal: ProCTAModal( + delegate: dependencies[singleton: .sessionProState], + variant: .morePinnedConvos( + isGrandfathered: (numPinnedConversations > LibSession.PinnedConversationLimit) + ), + dataManager: dependencies[singleton: .imageDataManager] + ) ) - ) - self?.transitionToScreen(sessionProModal, transitionType: .present) + self?.transitionToScreen(sessionProModal, transitionType: .present) + } } ) return diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 37258e84d0..eae0d7c756 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -306,7 +306,6 @@ public class HomeViewModel: NavigatableStateHolder { profileCache[eventValue.id] = userProfile } - /// Then handle database events if !dependencies[singleton: .storage].isSuspended, let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { do { @@ -383,8 +382,14 @@ public class HomeViewModel: NavigatableStateHolder { .fetchCount(db) } - /// Update loaded page info as needed - if loadPageEvent != nil || !insertedIds.isEmpty || !deletedIds.isEmpty { + /// Update loaded page info as needed (any change to a conversation could result in an order change so reload + /// the paged data if needed (as that will fetch the correct order) + if + loadPageEvent != nil || + !idsNeedingRequery.isEmpty || + !insertedIds.isEmpty || + !deletedIds.isEmpty + { loadResult = try loadResult.load( db, target: ( diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 48c7b34951..b95e0e5a39 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -539,14 +539,17 @@ struct MessageInfoScreen: View { guard dependencies[feature: .sessionProEnabled] && (!dependencies[cache: .libSession].isSessionPro) else { return } - let sessionProModal: ModalHostingViewController = ModalHostingViewController( - modal: ProCTAModal( - delegate: dependencies[singleton: .sessionProState], - variant: proCTAVariant, - dataManager: dependencies[singleton: .imageDataManager] + + DispatchQueue.main.async { + let sessionProModal: ModalHostingViewController = ModalHostingViewController( + modal: ProCTAModal( + delegate: dependencies[singleton: .sessionProState], + variant: proCTAVariant, + dataManager: dependencies[singleton: .imageDataManager] + ) ) - ) - self.host.controller?.present(sessionProModal, animated: true) + self.host.controller?.present(sessionProModal, animated: true) + } } func showUserProfileModal() { @@ -623,24 +626,26 @@ struct MessageInfoScreen: View { ) }() - let userProfileModal: ModalHostingViewController = ModalHostingViewController( - modal: UserProfileModal( - info: .init( - sessionId: sessionId, - blindedId: blindedId, - qrCodeImage: qrCodeImage, - profileInfo: profileInfo, - displayName: displayName, - contactDisplayName: contactDisplayName, - isProUser: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: messageViewModel.profile) }), - isMessageRequestsEnabled: isMessasgeRequestsEnabled, - onStartThread: self.onStartThread, - onProBadgeTapped: self.showSessionProCTAIfNeeded - ), - dataManager: dependencies[singleton: .imageDataManager] + DispatchQueue.main.async { + let userProfileModal: ModalHostingViewController = ModalHostingViewController( + modal: UserProfileModal( + info: .init( + sessionId: sessionId, + blindedId: blindedId, + qrCodeImage: qrCodeImage, + profileInfo: profileInfo, + displayName: displayName, + contactDisplayName: contactDisplayName, + isProUser: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: messageViewModel.profile) }), + isMessageRequestsEnabled: isMessasgeRequestsEnabled, + onStartThread: self.onStartThread, + onProBadgeTapped: self.showSessionProCTAIfNeeded + ), + dataManager: dependencies[singleton: .imageDataManager] + ) ) - ) - self.host.controller?.present(userProfileModal, animated: true, completion: nil) + self.host.controller?.present(userProfileModal, animated: true, completion: nil) + } } private func showMediaFullScreen(attachment: Attachment) { diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 1ad6a5d528..5a4c909c79 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -396,13 +396,15 @@ extension Onboarding { /// If we don't have the `Note to Self` thread then create it (not visible by default) if (try? SessionThread.exists(db, id: userSessionId.hexString)) != nil { - try SessionThread.upsert( - db, - id: userSessionId.hexString, - variant: .contact, - values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), - using: dependencies - ) + try ThreadCreationContext.$isOnboarding.withValue(true) { + try SessionThread.upsert( + db, + id: userSessionId.hexString, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + using: dependencies + ) + } } /// Update the `displayName` if changed @@ -440,8 +442,11 @@ extension Onboarding { } }, completion: { _ in - /// No need to show the seed again if the user is restoring - dependencies.setAsync(.hasViewedSeed, (initialFlow == .restore)) + /// No need to show the seed again if the user is restoring (just in case only set the value if it hasn't already + /// been set - this will prevent us from unintentionally re-showing the seed banner) + if !dependencies.mutate(cache: .libSession, { $0.has(.hasViewedSeed) }) { + dependencies.setAsync(.hasViewedSeed, (initialFlow == .restore)) + } /// Now that the onboarding process is completed we can store the `UserMetadata` for the Share and Notification /// extensions (prior to this point the account is in an invalid state so they can't be used) diff --git a/Session/Utilities/DonationsManager.swift b/Session/Utilities/DonationsManager.swift index 7f2589d175..e9fa74245b 100644 --- a/Session/Utilities/DonationsManager.swift +++ b/Session/Utilities/DonationsManager.swift @@ -57,7 +57,7 @@ public class DonationsManager { /// case we _do_ want to show it) let appInstallationDate: Date = { guard !dependencies.hasSet(feature: .customFirstInstallDateTime) else { - return Date(timeIntervalSince1970: dependencies[feature: .customFirstInstallDateTime] ?? 0) + return Date(timeIntervalSince1970: dependencies[feature: .customFirstInstallDateTime]) } let attributes: [FileAttributeKey: Any]? = try? dependencies[singleton: .fileManager] diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index e9c932b671..6b177272c8 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -227,14 +227,16 @@ public extension UIContextualAction { indexPath: indexPath, tableView: tableView ) { _, _, completionHandler in - if !isCurrentlyPinned, - !dependencies[cache: .libSession].isSessionPro, - let pinnedConversationsNumber: Int = dependencies[singleton: .storage].read({ db in - try SessionThread - .filter(SessionThread.Columns.pinnedPriority > 0) - .fetchCount(db) - }), - pinnedConversationsNumber >= LibSession.PinnedConversationLimit + if + dependencies[feature: .sessionProEnabled], + !isCurrentlyPinned, + !dependencies[cache: .libSession].isSessionPro, + let pinnedConversationsNumber: Int = dependencies[singleton: .storage].read({ db in + try SessionThread + .filter(SessionThread.Columns.pinnedPriority > 0) + .fetchCount(db) + }), + pinnedConversationsNumber >= LibSession.PinnedConversationLimit { let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( @@ -259,14 +261,13 @@ public extension UIContextualAction { // Delay the change to give the cell "unswipe" animation some time to complete DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { dependencies[singleton: .storage].writeAsync { db in - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAllAndConfig( - db, - SessionThread.Columns.pinnedPriority - .set(to: (isCurrentlyPinned ? 0 : 1)), - using: dependencies - ) + try SessionThread.updateVisibility( + db, + threadId: threadViewModel.threadId, + isVisible: true, + customPriority: (isCurrentlyPinned ? LibSession.visiblePriority : 1), + using: dependencies + ) } } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index c3893cbeb1..9cbc34aecc 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -146,11 +146,15 @@ public struct SessionThread: Codable, Identifiable, Equatable, Hashable, Fetchab public func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws { _ = try insert() - switch ObservationContext.observingDb { - case .none: Log.error("[SessionThread] Could not process 'aroundInsert' due to missing observingDb.") - case .some(let observingDb): - observingDb.dependencies.setAsync(.hasSavedThread, true) - observingDb.addConversationEvent(id: id, type: .created) + /// If this thread was created during onboarding then we don't want to set `hasSavedThread` or send a conversation + /// event (as this is likely the "Note to Self" thread or some future "initial" state which wasn't a user-driven change) + if ThreadCreationContext.isOnboarding != true { + switch ObservationContext.observingDb { + case .none: Log.error("[SessionThread] Could not process 'aroundInsert' due to missing observingDb.") + case .some(let observingDb): + observingDb.dependencies.setAsync(.hasSavedThread, true) + observingDb.addConversationEvent(id: id, type: .created) + } } } } @@ -883,3 +887,12 @@ public extension String { return truncated(prefix: 4, suffix: 4) } } + +// MARK: - ThreadCreationContext + +public enum ThreadCreationContext { + /// This `TaskLocal` variable is set and accessible within the context of a single `Task` and allows any code running within + /// the task to access the isntance without running into threading issues or needing to manage multiple instances + @TaskLocal + public static var isOnboarding: Bool? +} diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 86e2af1abc..1ac596f4aa 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -693,8 +693,8 @@ class OnboardingSpec: AsyncSpec { try ConfigDump.fetchAll(db) } - try require(result).to(haveCount(2)) - try require(Set((result?.map { $0.variant })!)).to(equal([.userProfile, .local])) + try require(result).to(haveCount(1)) + try require(Set((result?.map { $0.variant })!)).to(equal([.userProfile])) expect(result![0].variant).to(equal(.userProfile)) let userProfileDump: ConfigDump = (result?.first(where: { $0.variant == .userProfile }))! expect(userProfileDump.variant).to(equal(.userProfile)) diff --git a/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift b/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift index bf5a6f1a72..86801f7891 100644 --- a/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift +++ b/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift @@ -92,7 +92,7 @@ protocol ModalHostIdentifiable {} // MARK: - ModalHostingViewController open class ModalHostingViewController: UIHostingController>>, ModalHostIdentifiable where Content: View { - public init(modal: Content) { + @MainActor public init(modal: Content) { let container = HostWrapper() let modified = modal.environmentObject(container) as! ModifiedContent> super.init(rootView: modified)