From 5fdfd6df3b3054b478df3532fb60833346f8097e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 17 Mar 2023 15:12:35 +1100 Subject: [PATCH] Fixed issues raised during QA Fixed a bug where the legacy group invitation was getting sent to the wrong location Fixed a bug where outgoing typing indicators would be sent to blocked contacts Fixed a bug where the call button was visible for blocked contacts Fixed a bug where read receipts could be sent to blocked contacts Fixed a bug where the conversation nav buttons wouldn't get updated correctly in some cases Fixed a bug where we could incorrectly include the current user in the contacts syncing Fixed a bug where the initial state of the Note to Self conversation wasn't getting synced Fixed a bug where the Note to Self conversation could get removed Fixed a bug with where the conversation title would be misaligned in some cases Fixed a bug where link previews and quotes with images weren't getting sent correctly Fixed a crash when removing a user from a legacy group Added some missing accessibility info Updated the code to ensure the user is kicked from the conversation if it's deletion gets synced while it's open Updated the conversation empty state copy --- Session.xcodeproj/project.pbxproj | 4 + .../ConversationVC+Interaction.swift | 3 + Session/Conversations/ConversationVC.swift | 83 ++++++++++++++++--- .../Conversations/ConversationViewModel.swift | 19 +++++ .../Conversations/Input View/InputView.swift | 1 - .../Input View/MentionSelectionView.swift | 5 ++ .../ConversationTitleView.swift | 7 +- Session/Home/HomeVC.swift | 6 +- .../MessageRequestsViewController.swift | 17 +--- .../MessageRequestsViewModel.swift | 36 +++++++- .../ImagePickerController.swift | 3 +- .../Translations/de.lproj/Localizable.strings | 3 + .../Translations/en.lproj/Localizable.strings | 3 + .../Translations/es.lproj/Localizable.strings | 3 + .../Translations/fa.lproj/Localizable.strings | 3 + .../Translations/fi.lproj/Localizable.strings | 3 + .../Translations/fr.lproj/Localizable.strings | 3 + .../Translations/hi.lproj/Localizable.strings | 3 + .../Translations/hr.lproj/Localizable.strings | 3 + .../id-ID.lproj/Localizable.strings | 3 + .../Translations/it.lproj/Localizable.strings | 3 + .../Translations/ja.lproj/Localizable.strings | 3 + .../Translations/nl.lproj/Localizable.strings | 3 + .../Translations/pl.lproj/Localizable.strings | 3 + .../pt_BR.lproj/Localizable.strings | 3 + .../Translations/ru.lproj/Localizable.strings | 3 + .../Translations/si.lproj/Localizable.strings | 3 + .../Translations/sk.lproj/Localizable.strings | 3 + .../Translations/sv.lproj/Localizable.strings | 3 + .../Translations/th.lproj/Localizable.strings | 3 + .../vi-VN.lproj/Localizable.strings | 3 + .../zh-Hant.lproj/Localizable.strings | 3 + .../zh_CN.lproj/Localizable.strings | 3 + Session/Notifications/AppNotifications.swift | 48 +++++++---- Session/Settings/SettingsViewModel.swift | 2 +- .../_014_GenerateInitialUserConfigDumps.swift | 9 +- .../Database/Models/SessionThread.swift | 42 ++++++++++ .../SessionUtil+Contacts.swift | 9 ++ .../Config Handling/SessionUtil+Shared.swift | 55 +++++++++++- .../SessionUtil+UserGroups.swift | 18 ++-- .../MessageReceiver+TypingIndicators.swift | 24 +++++- .../MessageReceiver+VisibleMessages.swift | 8 +- .../MessageSender+ClosedGroups.swift | 21 +++-- .../Sending & Receiving/MessageSender.swift | 8 +- .../Typing Indicators/TypingIndicators.swift | 12 ++- .../SessionThreadViewModel.swift | 17 +++- .../Utilities/FetchRequest+Utilities.swift | 11 +++ .../Utilities/UIViewController+OWS.swift | 10 +++ 48 files changed, 463 insertions(+), 81 deletions(-) create mode 100644 SessionUtilitiesKit/Database/Utilities/FetchRequest+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 095fd6a76..ef352545d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -818,6 +818,7 @@ FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; + FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; }; FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; }; FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; }; @@ -1946,6 +1947,7 @@ FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; + FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = ""; }; FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; @@ -3718,6 +3720,7 @@ FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */, FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */, + FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, ); @@ -5642,6 +5645,7 @@ FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */, + FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, B87EF18126377A1D00124B3C /* Features.swift in Sources */, FD09797727FAB7A600936362 /* Data+Image.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 28f27bfa0..dac18e737 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -64,6 +64,7 @@ extension ConversationVC: @objc func startCall(_ sender: Any?) { guard SessionCall.isEnabled else { return } + guard viewModel.threadData.threadIsBlocked == false else { return } guard Storage.shared[.areCallsEnabled] else { let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( @@ -678,9 +679,11 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadIsMessageRequest: Bool = (self.viewModel.threadData.threadIsMessageRequest == true) + let threadIsBlocked: Bool = (self.viewModel.threadData.threadIsBlocked == true) let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart( threadId: threadId, threadVariant: threadVariant, + threadIsBlocked: threadIsBlocked, threadIsMessageRequest: threadIsMessageRequest, direction: .outgoing, timestampMs: SnodeAPI.currentOffsetTimestampMs() diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e94794001..35cf9ea39 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -9,7 +9,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate { +final class ConversationVC: BaseVC, SessionUtilRespondingViewController, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate { private static let loadingHeaderHeight: CGFloat = 40 internal let viewModel: ConversationViewModel @@ -211,8 +211,14 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl private lazy var emptyStateLabel: UILabel = { let text: String = String( - format: "CONVERSATION_EMPTY_STATE".localized(), - self.viewModel.threadData.displayName + format: { + switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) { + case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() + case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() + default: return "CONVERSATION_EMPTY_STATE".localized() + } + }(), + viewModel.threadData.displayName ) let result: UILabel = UILabel() @@ -385,8 +391,16 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // nav will be offset incorrectly during the push animation (unfortunately the profile icon still // doesn't appear until after the animation, I assume it's taking a snapshot or something, but // there isn't much we can do about that unfortunately) - updateNavBarButtons(threadData: nil, initialVariant: self.viewModel.initialThreadVariant) - titleView.initialSetup(with: self.viewModel.initialThreadVariant) + updateNavBarButtons( + threadData: nil, + initialVariant: self.viewModel.initialThreadVariant, + initialIsNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf, + initialIsBlocked: (self.viewModel.threadData.threadIsBlocked == true) + ) + titleView.initialSetup( + with: self.viewModel.initialThreadVariant, + isNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf + ) // Constraints view.addSubview(tableView) @@ -533,6 +547,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl let threadId: String = viewModel.threadData.threadId if + viewModel.threadData.threadIsNoteToSelf == false && viewModel.threadData.threadShouldBeVisible == false && !SessionUtil.conversationExistsInConfig( threadId: threadId, @@ -674,9 +689,16 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // Update the empty state let text: String = String( - format: "CONVERSATION_EMPTY_STATE".localized(), + format: { + switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) { + case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() + case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() + default: return "CONVERSATION_EMPTY_STATE".localized() + } + }(), updatedThreadData.displayName ) + emptyStateLabel.attributedText = NSAttributedString(string: text) .adding( attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)], @@ -689,11 +711,17 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl if initialLoad || viewModel.threadData.threadVariant != updatedThreadData.threadVariant || + viewModel.threadData.threadIsBlocked != updatedThreadData.threadIsBlocked || viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval || viewModel.threadData.threadIsMessageRequest != updatedThreadData.threadIsMessageRequest || viewModel.threadData.profile != updatedThreadData.profile { - updateNavBarButtons(threadData: updatedThreadData, initialVariant: viewModel.initialThreadVariant) + updateNavBarButtons( + threadData: updatedThreadData, + initialVariant: viewModel.initialThreadVariant, + initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf, + initialIsBlocked: (viewModel.threadData.threadIsBlocked == true) + ) let messageRequestsViewWasVisible: Bool = ( messageRequestStackView.isHidden == false @@ -1139,7 +1167,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } } - func updateNavBarButtons(threadData: SessionThreadViewModel?, initialVariant: SessionThread.Variant) { + func updateNavBarButtons( + threadData: SessionThreadViewModel?, + initialVariant: SessionThread.Variant, + initialIsNoteToSelf: Bool, + initialIsBlocked: Bool + ) { navigationItem.hidesBackButton = isShowingSearchUI if isShowingSearchUI { @@ -1147,6 +1180,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl navigationItem.rightBarButtonItems = [] } else { + let shouldHaveCallButton: Bool = ( + SessionCall.isEnabled && + (threadData?.threadVariant ?? initialVariant) == .contact && + (threadData?.threadIsNoteToSelf ?? initialIsNoteToSelf) == false && + (threadData?.threadIsBlocked ?? initialIsBlocked) == false + ) + guard let threadData: SessionThreadViewModel = threadData, ( @@ -1169,7 +1209,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl ) ) ), - (initialVariant == .contact ? + (shouldHaveCallButton ? UIBarButtonItem(customView: UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))) : nil ) @@ -1199,7 +1239,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl settingsButtonItem.accessibilityLabel = "More options" settingsButtonItem.isAccessibilityElement = true - if SessionCall.isEnabled && !threadData.threadIsNoteToSelf { + if shouldHaveCallButton { let callButton = UIBarButtonItem( image: UIImage(named: "Phone"), style: .plain, @@ -1207,11 +1247,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl action: #selector(startCall) ) callButton.accessibilityLabel = "Call button" + callButton.isAccessibilityElement = true navigationItem.rightBarButtonItems = [settingsButtonItem, callButton] } else { - navigationItem.rightBarButtonItem = settingsButtonItem + navigationItem.rightBarButtonItems = [settingsButtonItem] } default: @@ -1640,7 +1681,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } // Nav bar buttons - updateNavBarButtons(threadData: self.viewModel.threadData, initialVariant: viewModel.initialThreadVariant) + updateNavBarButtons( + threadData: viewModel.threadData, + initialVariant: viewModel.initialThreadVariant, + initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf, + initialIsBlocked: (viewModel.threadData.threadIsBlocked == true) + ) // Hack so that the ResultsBar stays on the screen when dismissing the search field // keyboard. @@ -1675,7 +1721,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl @objc func hideSearchUI() { isShowingSearchUI = false navigationItem.titleView = titleView - updateNavBarButtons(threadData: self.viewModel.threadData, initialVariant: viewModel.initialThreadVariant) + updateNavBarButtons( + threadData: viewModel.threadData, + initialVariant: viewModel.initialThreadVariant, + initialIsNoteToSelf: viewModel.threadData.threadIsNoteToSelf, + initialIsBlocked: (viewModel.threadData.threadIsBlocked == true) + ) searchController.uiSearchController.stubbableSearchBar.stubbedNextResponder = nil becomeFirstResponder() @@ -1800,4 +1851,10 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl .highlight() } } + + // MARK: - SessionUtilRespondingViewController + + func isConversation(in threadIds: [String]) -> Bool { + return threadIds.contains(self.viewModel.threadData.threadId) + } } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index b146b256a..f64664135 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -111,6 +111,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { threadId: self.threadId, threadVariant: self.initialThreadVariant, threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()), + threadIsBlocked: (self.initialThreadVariant != .contact ? false : + Storage.shared.read { db in + try Contact + .filter(id: self.threadId) + .select(.isBlocked) + .asRequest(of: Bool.self) + .fetchOne(db) + .defaulting(to: false) + } + ), currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyGroup && self.initialThreadVariant != .group) ? nil : Storage.shared.read { db in @@ -120,6 +130,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { .filter(GroupMember.Columns.role == GroupMember.Role.standard) .isNotEmpty(db) } + ), + openGroupPermissions: (self.initialThreadVariant != .community ? nil : + Storage.shared.read { db in + try OpenGroup + .filter(id: threadId) + .select(.permissions) + .asRequest(of: OpenGroup.Permissions.self) + .fetchOne(db) + } ) ) .populatingCurrentUserBlindedKey() diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index d0fc58d77..eeb0632cf 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -93,7 +93,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M let result: UIView = UIView() result.accessibilityLabel = "Mentions list" result.accessibilityIdentifier = "Mentions list" - result.isAccessibilityElement = true result.alpha = 0 let backgroundView = UIView() diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index b89cd884f..3ad8be5ba 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -92,6 +92,11 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele ), isLast: (indexPath.row == (candidates.count - 1)) ) + cell.accessibilityIdentifier = "Contact" + cell.accessibilityLabel = candidates[indexPath.row].profile.displayName( + for: candidates[indexPath.row].threadVariant + ) + cell.isAccessibilityElement = true return cell } diff --git a/Session/Conversations/Views & Modals/ConversationTitleView.swift b/Session/Conversations/Views & Modals/ConversationTitleView.swift index 84a24c3fe..69c695b0b 100644 --- a/Session/Conversations/Views & Modals/ConversationTitleView.swift +++ b/Session/Conversations/Views & Modals/ConversationTitleView.swift @@ -71,10 +71,13 @@ final class ConversationTitleView: UIView { // MARK: - Content - public func initialSetup(with threadVariant: SessionThread.Variant) { + public func initialSetup( + with threadVariant: SessionThread.Variant, + isNoteToSelf: Bool + ) { self.update( with: " ", - isNoteToSelf: false, + isNoteToSelf: isNoteToSelf, threadVariant: threadVariant, mutedUntilTimestamp: nil, onlyNotifyForMentions: false, diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index ae619d872..aed1ff06e 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -8,7 +8,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate { +final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate { private static let loadingHeaderHeight: CGFloat = 40 public static let newConversationButtonSize: CGFloat = 60 @@ -20,6 +20,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi private var isAutoLoadingNextPage: Bool = false private var viewHasAppeared: Bool = false + // MARK: - SessionUtilRespondingViewController + + let isConversationList: Bool = true + // MARK: - Intialization init() { diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 268878979..b9be340a0 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -481,19 +481,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat title: "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON".localized(), style: .destructive ) { _ in - // Clear the requests - Storage.shared.write { db in - _ = try SessionThread - .filter(ids: contactThreadIds) - .deleteAll(db) - - try ClosedGroup.removeKeysAndUnsubscribe( - db, - threadIds: closedGroupThreadIds, - removeGroupData: true, - calledFromConfigHandling: false - ) - } + MessageRequestsViewModel.clearAllRequests( + contactThreadIds: contactThreadIds, + closedGroupThreadIds: closedGroupThreadIds + ) }) alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 85c31dbda..9a79a13e4 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -186,7 +186,10 @@ public class MessageRequestsViewModel { ) { _ in Storage.shared.write { db in switch threadVariant { - case .contact, .community: + case .contact: + try SessionUtil + .hide(db, contactIds: [threadId]) + _ = try SessionThread .filter(id: threadId) .deleteAll(db) @@ -201,6 +204,8 @@ public class MessageRequestsViewModel { // Trigger a config sync ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db)) + + default: break } } @@ -245,6 +250,10 @@ public class MessageRequestsViewModel { Contact.Columns.didApproveMe.set(to: true) ) + // Sync the removal of the thread from other devices + try SessionUtil + .hide(db, contactIds: [threadId]) + // Remove the thread _ = try SessionThread .filter(id: threadId) @@ -257,4 +266,29 @@ public class MessageRequestsViewModel { viewController?.present(modal, animated: true, completion: nil) } + + static func clearAllRequests( + contactThreadIds: [String], + closedGroupThreadIds: [String] + ) { + // Clear the requests + Storage.shared.write { db in + // Sync the removal of the thread from other devices + try SessionUtil + .hide(db, contactIds: contactThreadIds) + + // Remove the threads + _ = try SessionThread + .filter(ids: contactThreadIds) + .deleteAll(db) + + // Remove the groups + try ClosedGroup.removeKeysAndUnsubscribe( + db, + threadIds: closedGroupThreadIds, + removeGroupData: true, + calledFromConfigHandling: false + ) + } + } } diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index bb3bd5d42..504465c31 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -533,7 +533,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath) let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) cell.configure(item: assetItem) - + cell.isAccessibilityElement = true + cell.accessibilityIdentifier = "\(assetItem.asset.modificationDate.map { "\($0)" } ?? "Unknown Date")" cell.isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset) return cell diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 519a42b20..31fa91ebb 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index d2bb87bd7..55a7dffc6 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 91484888d..50e0d517e 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index d764eb276..43eece40a 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 84c99f71c..2f924fbee 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 67c8dda6a..8b8ecd21c 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index cf6d01b55..0640cdf3e 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 505787d9e..c7f523d91 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index e6f6ff426..9e46d0bd8 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 36c15a86f..cdbe55283 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 92fdf76b8..3c6f81e33 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 885a4f8e6..7e2504992 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 3282638c4..8dcefccaa 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index ea550a3bd..a7f2d55de 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 53e0b8003..2145e6d83 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 4d5cdb2c3..e0a48dadd 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 6db1a875d..e295451a2 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index d8bf60419..0f4f5d75e 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index f9056d0ec..0772e09f5 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 1d7092db3..8ee7eabca 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index d25216d30..9ad9427bb 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index bfae91d61..c594cfcfe 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -614,4 +614,7 @@ "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; +"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; +"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; +"UPDATE_PROFILE_TITLE" = "Update Profile Picture"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 0e374700f..181598896 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -517,13 +517,13 @@ class NotificationActionHandler { return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) .eraseToAnyPublisher() } - - guard let thread: SessionThread = Storage.shared.read({ db in try SessionThread.fetchOne(db, id: threadId) }) else { + + guard Storage.shared.read({ db in try SessionThread.exists(db, id: threadId) }) == true else { return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)")) .eraseToAnyPublisher() } - return markAsRead(thread: thread) + return markAsRead(threadId: threadId) } func reply(userInfo: [AnyHashable: Any], replyText: String) -> AnyPublisher { @@ -540,7 +540,7 @@ class NotificationActionHandler { return Storage.shared .writePublisher(receiveOn: DispatchQueue.main) { db in let interaction: Interaction = try Interaction( - threadId: thread.id, + threadId: threadId, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: replyText, @@ -557,16 +557,20 @@ class NotificationActionHandler { try Interaction.markAsRead( db, interactionId: interaction.id, - threadId: thread.id, + threadId: threadId, threadVariant: thread.variant, includingOlder: true, - trySendReadReceipt: true + trySendReadReceipt: try SessionThread.canSendReadReceipt( + db, + threadId: threadId, + threadVariant: thread.variant + ) ) return try MessageSender.preparedSendData( db, interaction: interaction, - threadId: thread.id, + threadId: threadId, threadVariant: thread.variant ) } @@ -605,20 +609,34 @@ class NotificationActionHandler { .eraseToAnyPublisher() } - private func markAsRead(thread: SessionThread) -> AnyPublisher { + private func markAsRead(threadId: String) -> AnyPublisher { return Storage.shared .writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in - try Interaction.markAsRead( - db, - interactionId: try thread.interactions + guard + let threadVariant: SessionThread.Variant = try SessionThread + .filter(id: threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db), + let lastInteractionId: Int64 = try Interaction .select(.id) + .filter(Interaction.Columns.threadId == threadId) .order(Interaction.Columns.timestampMs.desc) .asRequest(of: Int64.self) - .fetchOne(db), - threadId: thread.id, - threadVariant: thread.variant, + .fetchOne(db) + else { throw NotificationError.failDebug("unable to required thread info: \(threadId)") } + + try Interaction.markAsRead( + db, + interactionId: lastInteractionId, + threadId: threadId, + threadVariant: threadVariant, includingOlder: true, - trySendReadReceipt: true + trySendReadReceipt: try SessionThread.canSendReadReceipt( + db, + threadId: threadId, + threadVariant: threadVariant + ) ) } .eraseToAnyPublisher() diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 9ceaea9b6..06c882916 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -487,7 +487,7 @@ class SettingsViewModel: SessionTableViewModel Bool { + let threadVariant: SessionThread.Variant = try { + try maybeThreadVariant ?? + SessionThread + .filter(id: threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db, orThrow: StorageError.objectNotFound) + }() + let threadIsBlocked: Bool = try { + try maybeIsBlocked ?? + ( + threadVariant == .contact && + Contact + .filter(id: threadId) + .select(.isBlocked) + .asRequest(of: Bool.self) + .fetchOne(db, orThrow: StorageError.objectNotFound) + ) + }() + let threadIsMessageRequest: Bool = SessionThread + .filter(id: threadId) + .filter( + SessionThread.isMessageRequest( + userPublicKey: getUserHexEncodedPublicKey(db), + includeNonVisible: true + ) + ) + .isNotEmpty(db) + + return ( + !threadIsBlocked && + !threadIsMessageRequest + ) + } + @available(*, unavailable, message: "should not be used until pin re-ordering is built") static func refreshPinnedPriorities(_ db: Database, adding threadId: String) throws { struct PinnedPriority: TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Contacts.swift index 3b5812119..a48c82259 100644 --- a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -168,6 +168,8 @@ internal extension SessionUtil { switch (data.shouldBeVisible, threadExists) { case (false, true): + SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: [contact.id]) + try SessionThread .filter(id: contact.id) .deleteAll(db) @@ -212,6 +214,8 @@ internal extension SessionUtil { .fetchAll(db) if !contactIdsToRemove.isEmpty { + SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: contactIdsToRemove) + try Contact .filter(ids: contactIdsToRemove) .deleteAll(db) @@ -224,6 +228,11 @@ internal extension SessionUtil { Profile.Columns.nickname.set(to: nil) ) + // Delete the one-to-one conversations associated to the contact + try SessionThread + .filter(ids: contactIdsToRemove) + .deleteAll(db) + try SessionUtil.remove(db, volatileContactIds: contactIdsToRemove) } } diff --git a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Shared.swift b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Shared.swift index e82fc39a2..5f0bae6ea 100644 --- a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Shared.swift +++ b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+Shared.swift @@ -1,7 +1,8 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit import GRDB +import SessionUIKit import SessionUtil import SessionUtilitiesKit @@ -191,6 +192,44 @@ internal extension SessionUtil { return updated } + + static func kickFromConversationUIIfNeeded(removedThreadIds: [String]) { + guard !removedThreadIds.isEmpty else { return } + + // If the user is currently navigating somewhere within the view hierarchy of a conversation + // we just deleted then return to the home screen + DispatchQueue.main.async { + guard + let rootViewController: UIViewController = CurrentAppContext().mainWindow?.rootViewController, + let topBannerController: TopBannerController = (rootViewController as? TopBannerController), + !topBannerController.children.isEmpty, + let navController: UINavigationController = topBannerController.children[0] as? UINavigationController + else { return } + + // Extract the ones which will respond to SessionUtil changes + let targetViewControllers: [any SessionUtilRespondingViewController] = navController + .viewControllers + .compactMap({ $0 as? SessionUtilRespondingViewController }) + + // Make sure we have a conversation list and that one of the removed conversations are + // in the nav hierarchy + guard + targetViewControllers.count > 1, + targetViewControllers.contains(where: { $0.isConversationList }), + targetViewControllers.contains(where: { $0.isConversation(in: removedThreadIds) }) + else { return } + + // Return to the root view controller as the removed conversation will be invalid + if navController.presentedViewController != nil { + navController.dismiss(animated: false) { + navController.popToRootViewController(animated: true) + } + } + else { + navController.popToRootViewController(animated: true) + } + } + } } // MARK: - External Outgoing Changes @@ -285,3 +324,17 @@ extension SessionUtil { let shouldBeVisible: Bool } } + +// MARK: - SessionUtilRespondingViewController + +public protocol SessionUtilRespondingViewController { + var isConversationList: Bool { get } + + func isConversation(in threadIds: [String]) -> Bool +} + +public extension SessionUtilRespondingViewController { + var isConversationList: Bool { false } + + func isConversation(in threadIds: [String]) -> Bool { return false } +} diff --git a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserGroups.swift b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserGroups.swift index 69cd956f8..bda053c0e 100644 --- a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserGroups.swift +++ b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserGroups.swift @@ -168,12 +168,16 @@ internal extension SessionUtil { .keys) .subtracting(communities.map { $0.data.threadId }) - communityIdsToRemove.forEach { threadId in - OpenGroupManager.shared.delete( - db, - openGroupId: threadId, - calledFromConfigHandling: true - ) + if !communityIdsToRemove.isEmpty { + SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(communityIdsToRemove)) + + communityIdsToRemove.forEach { threadId in + OpenGroupManager.shared.delete( + db, + openGroupId: threadId, + calledFromConfigHandling: true + ) + } } // MARK: -- Handle Legacy Group Changes @@ -320,6 +324,8 @@ internal extension SessionUtil { .subtracting(legacyGroups.map { $0.id }) if !legacyGroupIdsToRemove.isEmpty { + SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove)) + try ClosedGroup.removeKeysAndUnsubscribe( db, threadIds: Array(legacyGroupIdsToRemove), diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift index 4d4c39329..91a00bd41 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+TypingIndicators.swift @@ -11,24 +11,40 @@ extension MessageReceiver { threadVariant: SessionThread.Variant, message: TypingIndicator ) throws { - guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return } + guard try SessionThread.exists(db, id: threadId) else { return } switch message.kind { case .started: + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let threadIsBlocked: Bool = ( + threadVariant == .contact && + (try? Contact + .filter(id: threadId) + .select(.isBlocked) + .asRequest(of: Bool.self) + .fetchOne(db)) + .defaulting(to: false) + ) + let threadIsMessageRequest: Bool = (try? SessionThread + .filter(id: threadId) + .filter(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: true)) + .isEmpty(db)) + .defaulting(to: false) let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart( threadId: threadId, threadVariant: threadVariant, - threadIsMessageRequest: thread.isMessageRequest(db), + threadIsBlocked: threadIsBlocked, + threadIsMessageRequest: threadIsMessageRequest, direction: .incoming, timestampMs: message.sentTimestamp.map { Int64($0) } ) if needsToStartTypingIndicator { - TypingIndicators.start(db, threadId: thread.id, direction: .incoming) + TypingIndicators.start(db, threadId: threadId, direction: .incoming) } case .stopped: - TypingIndicators.didStopTyping(db, threadId: thread.id, direction: .incoming) + TypingIndicators.didStopTyping(db, threadId: threadId, direction: .incoming) default: SNLog("Unknown TypingIndicator Kind ignored") diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 0dc6acc39..a891bb3ba 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -196,7 +196,7 @@ extension MessageReceiver { // If we receive an outgoing message that already exists in the database // then we still need up update the recipient and read states for the // message (even if we don't need to do anything else) - try updateRecipientAndReadStates( + try updateRecipientAndReadStatesForOutgoingInteraction( db, thread: thread, interactionId: existingInteractionId, @@ -214,7 +214,7 @@ extension MessageReceiver { guard let interactionId: Int64 = interaction.id else { throw StorageError.failedToSave } // Update and recipient and read states as needed - try updateRecipientAndReadStates( + try updateRecipientAndReadStatesForOutgoingInteraction( db, thread: thread, interactionId: interactionId, @@ -398,7 +398,7 @@ extension MessageReceiver { return interactionId } - private static func updateRecipientAndReadStates( + private static func updateRecipientAndReadStatesForOutgoingInteraction( _ db: Database, thread: SessionThread, interactionId: Int64, @@ -454,7 +454,7 @@ extension MessageReceiver { threadId: thread.id, threadVariant: thread.variant, includingOlder: true, - trySendReadReceipt: true + trySendReadReceipt: false ) // Process any PendingReadReceipt values diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index a8cfb64bf..da4f59e7a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -455,8 +455,8 @@ extension MessageSender { ) ), interactionId: nil, - threadId: closedGroup.threadId, - threadVariant: .legacyGroup + threadId: member, + threadVariant: .contact ) // Add the users to the group @@ -548,13 +548,16 @@ extension MessageSender { ) ) .flatMap { _ -> AnyPublisher in - generateAndSendNewEncryptionKeyPair( - db, - targetMembers: members, - userPublicKey: userPublicKey, - allGroupMembers: allGroupMembers, - closedGroup: closedGroup - ) + Storage.shared + .writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in + generateAndSendNewEncryptionKeyPair( + db, + targetMembers: members, + userPublicKey: userPublicKey, + allGroupMembers: allGroupMembers, + closedGroup: closedGroup + ) + } } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 72b4d05d5..f861fad72 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -599,7 +599,13 @@ public final class MessageSender { // `MessageSender.performUploadsIfNeeded(queue:preparedSendData:)` before calling this function switch preparedSendData.message { case let visibleMessage as VisibleMessage: - guard visibleMessage.attachmentIds.count == preparedSendData.totalAttachmentsUploaded else { + let expectedAttachmentUploadCount: Int = ( + visibleMessage.attachmentIds.count + + (visibleMessage.linkPreview?.attachmentId != nil ? 1 : 0) + + (visibleMessage.quote?.attachmentId != nil ? 1 : 0) + ) + + guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else { return Fail(error: MessageSenderError.attachmentsNotUploaded) .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index d3891d087..b8c4d3b49 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -25,6 +25,7 @@ public class TypingIndicators { init?( threadId: String, threadVariant: SessionThread.Variant, + threadIsBlocked: Bool, threadIsMessageRequest: Bool, direction: Direction, timestampMs: Int64? @@ -34,9 +35,11 @@ public class TypingIndicators { // or show typing indicators for other users // // We also don't want to show/send typing indicators for message requests - guard Storage.shared[.typingIndicatorsEnabled] && !threadIsMessageRequest else { - return nil - } + guard + Storage.shared[.typingIndicatorsEnabled] && + !threadIsBlocked && + !threadIsMessageRequest + else { return nil } // Don't send typing indicators in group threads guard @@ -143,6 +146,7 @@ public class TypingIndicators { public static func didStartTypingNeedsToStart( threadId: String, threadVariant: SessionThread.Variant, + threadIsBlocked: Bool, threadIsMessageRequest: Bool, direction: Direction, timestampMs: Int64? @@ -159,6 +163,7 @@ public class TypingIndicators { let newIndicator: Indicator? = Indicator( threadId: threadId, threadVariant: threadVariant, + threadIsBlocked: threadIsBlocked, threadIsMessageRequest: threadIsMessageRequest, direction: direction, timestampMs: timestampMs @@ -179,6 +184,7 @@ public class TypingIndicators { let newIndicator: Indicator? = Indicator( threadId: threadId, threadVariant: threadVariant, + threadIsBlocked: threadIsBlocked, threadIsMessageRequest: threadIsMessageRequest, direction: direction, timestampMs: timestampMs diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 94f9bdd89..a10542eaf 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -279,7 +279,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat let threadId: String = self.threadId let threadVariant: SessionThread.Variant = self.threadVariant - let trySendReadReceipt: Bool = (self.threadIsMessageRequest == false) + let threadIsBlocked: Bool? = self.threadIsBlocked + let threadIsMessageRequest: Bool? = self.threadIsMessageRequest Storage.shared.writeAsync { db in // Only make this change if needed (want to avoid triggering a thread update @@ -299,7 +300,13 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat threadId: threadId, threadVariant: threadVariant, includingOlder: true, - trySendReadReceipt: trySendReadReceipt + trySendReadReceipt: try SessionThread.canSendReadReceipt( + db, + threadId: threadId, + threadVariant: threadVariant, + isBlocked: threadIsBlocked, + isMessageRequest: threadIsMessageRequest + ) ) } } @@ -332,8 +339,10 @@ public extension SessionThreadViewModel { threadId: String? = nil, threadVariant: SessionThread.Variant? = nil, threadIsNoteToSelf: Bool = false, + threadIsBlocked: Bool? = nil, contactProfile: Profile? = nil, currentUserIsClosedGroupMember: Bool? = nil, + openGroupPermissions: OpenGroup.Permissions? = nil, unreadCount: UInt = 0 ) { self.rowId = -1 @@ -347,7 +356,7 @@ public extension SessionThreadViewModel { self.threadRequiresApproval = false self.threadShouldBeVisible = false self.threadPinnedPriority = 0 - self.threadIsBlocked = nil + self.threadIsBlocked = threadIsBlocked self.threadMutedUntilTimestamp = nil self.threadOnlyNotifyForMentions = nil self.threadMessageDraft = nil @@ -373,7 +382,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/SessionUtilitiesKit/Database/Utilities/FetchRequest+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/FetchRequest+Utilities.swift new file mode 100644 index 000000000..a3bccd855 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/FetchRequest+Utilities.swift @@ -0,0 +1,11 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import GRDB + +public extension FetchRequest where RowDecoder: DatabaseValueConvertible { + func fetchOne(_ db: Database, orThrow error: Error) throws -> RowDecoder { + guard let result: RowDecoder = try fetchOne(db) else { throw error } + + return result + } +} diff --git a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift index c9762da43..6791a15e1 100644 --- a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift @@ -14,6 +14,16 @@ public extension UIViewController { var nextViewController: UIViewController? = viewController.presentedViewController + if + let topBannerController: TopBannerController = nextViewController as? TopBannerController, + !topBannerController.children.isEmpty + { + nextViewController = ( + topBannerController.children[0].presentedViewController ?? + topBannerController.children[0] + ) + } + if let nextViewController: UIViewController = nextViewController { if !ignoringAlerts || !(nextViewController is UIAlertController) { if visitedViewControllers.contains(nextViewController) {