diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c9bbee887..8200e4d4c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5288,7 +5288,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5309,7 +5309,7 @@ INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5357,7 +5357,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5383,7 +5383,7 @@ INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5418,7 +5418,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5437,7 +5437,7 @@ INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5488,7 +5488,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5512,7 +5512,7 @@ INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6507,7 +6507,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6543,7 +6543,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6575,7 +6575,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 158; + CURRENT_PROJECT_VERSION = 163; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6611,7 +6611,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.7.3; + MARKETING_VERSION = 1.7.5; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 0d6b18877..50cd6f446 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -257,6 +257,9 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega return showError(title: "Couldn't Update Group", message: "Can't leave while adding or removing other members.") } } + guard members.count <= 100 else { + return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: "")) + } Storage.write(with: { [weak self] transaction in do { if !members.contains(getUserHexEncodedPublicKey()) { diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 4c6b0363d..9cfaf5a23 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -163,11 +163,12 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat guard selectedContacts.count >= 1 else { return showError(title: "Please pick at least 1 group member") } - guard selectedContacts.count < 20 else { // Minus one because we're going to include self later + guard selectedContacts.count < 100 else { // Minus one because we're going to include self later return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: "")) } let selectedContacts = self.selectedContacts - ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in + let message: String? = (selectedContacts.count > 20) ? "Please wait while the group is created..." : nil + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in var promise: Promise! Storage.writeSync { transaction in promise = MessageSender.createClosedGroup(name: name, members: selectedContacts, transaction: transaction) diff --git a/Session/Conversations/ConversationViewController.m b/Session/Conversations/ConversationViewController.m index 7dd831377..b7f2b3a53 100644 --- a/Session/Conversations/ConversationViewController.m +++ b/Session/Conversations/ConversationViewController.m @@ -1528,15 +1528,17 @@ typedef enum : NSUInteger { [self showDetailViewForViewItem:conversationViewItem]; } -- (void)report:(id)conversationViewItem +- (void)banUser:(id)conversationViewItem { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Report?" message:@"If the message is found to violate the Session Public Chat code of conduct it will be removed." preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - uint64_t messageID = 0; - if ([conversationViewItem.interaction isKindOfClass:TSMessage.class]) { - messageID = ((TSMessage *)conversationViewItem.interaction).openGroupServerMessageID; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Ban This User?" message:nil preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + NSString* publicKey; + if ([conversationViewItem.interaction isKindOfClass:TSIncomingMessage.class]) { + publicKey = ((TSIncomingMessage *)conversationViewItem.interaction).authorId; } - [SNOpenGroupAPI reportMessageWithID:messageID inChannel:1 onServer:@"https://chat.getsession.org"]; + SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:self.thread.uniqueId]; + if (openGroup == nil) return; + [[SNOpenGroupAPI banPublicKey:publicKey fromServer:openGroup.server] retainUntilComplete]; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; @@ -2537,6 +2539,7 @@ typedef enum : NSUInteger { [ModalActivityIndicatorViewController presentFromViewController:self canCancel:YES + message:nil backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { DataSource *dataSource = [DataSourcePath dataSourceWithURL:movieURL shouldDeleteOnDeallocation:NO]; diff --git a/Session/Conversations/ConversationViewItem.h b/Session/Conversations/ConversationViewItem.h index 2d45d5eea..45f91a697 100644 --- a/Session/Conversations/ConversationViewItem.h +++ b/Session/Conversations/ConversationViewItem.h @@ -67,6 +67,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly) BOOL isGroupThread; @property (nonatomic, readonly) BOOL userCanDeleteGroupMessage; +@property (nonatomic, readonly) BOOL userHasModerationPermission; @property (nonatomic, readonly) BOOL hasBodyText; diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index bbca5efbb..75e2b9ceb 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -1162,30 +1162,45 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) - (BOOL)userCanDeleteGroupMessage { if (!self.isGroupThread) return false; - - // Ensure the thread is a public chat and not an RSS feed TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; // Only allow deletion on incoming and outgoing messages OWSInteractionType interationType = self.interaction.interactionType; if (interationType != OWSInteractionType_OutgoingMessage && interationType != OWSInteractionType_IncomingMessage) return false; - // Make sure it's a public chat message + // Make sure it's an open group message TSMessage *message = (TSMessage *)self.interaction; if (!message.isOpenGroupMessage) return true; // Ensure we have the details needed to contact the server - SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId]; - if (publicChat == nil) return true; + SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId]; + if (openGroup == nil) return true; if (interationType == OWSInteractionType_IncomingMessage) { // Only allow deletion on incoming messages if the user has moderation permission - return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forChannel:publicChat.channel onServer:publicChat.server]; + return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forChannel:openGroup.channel onServer:openGroup.server]; } else { return YES; } } +- (BOOL)userHasModerationPermission +{ + if (!self.isGroupThread) return false; + TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; + + // Make sure it's an open group message + TSMessage *message = (TSMessage *)self.interaction; + if (!message.isOpenGroupMessage) return false; + + // Ensure we have the details needed to contact the server + SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId]; + if (openGroup == nil) return false; + + // Check that we're a moderator + return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forChannel:openGroup.channel onServer:openGroup.server]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Session/Conversations/MenuActionsViewController.swift b/Session/Conversations/MenuActionsViewController.swift index f9357a4b8..15ac746d3 100644 --- a/Session/Conversations/MenuActionsViewController.swift +++ b/Session/Conversations/MenuActionsViewController.swift @@ -311,6 +311,7 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { case .failed: Logger.debug("failed") unhighlightAllActionViews() + default: break } } diff --git a/Session/Conversations/MessageActions.swift b/Session/Conversations/MessageActions.swift index d049b7c20..96fe41dc5 100644 --- a/Session/Conversations/MessageActions.swift +++ b/Session/Conversations/MessageActions.swift @@ -6,7 +6,7 @@ import Foundation @objc protocol MessageActionsDelegate: class { - func report(_ conversationViewItem: ConversationViewItem) + func banUser(_ conversationViewItem: ConversationViewItem) func messageActionsShowDetailsForItem(_ conversationViewItem: ConversationViewItem) func messageActionsReplyToItem(_ conversationViewItem: ConversationViewItem) func copyPublicKey(for conversationViewItem: ConversationViewItem) @@ -45,14 +45,6 @@ struct MessageActionBuilder { block: { [weak delegate] _ in delegate?.messageActionsShowDetailsForItem(conversationViewItem) } ) } - - static func report(_ conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction { - return MenuAction(image: #imageLiteral(resourceName: "Flag"), - title: NSLocalizedString("Report", comment: ""), - subtitle: nil, - block: { [weak delegate] _ in delegate?.report(conversationViewItem) } - ) - } static func deleteMessage(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction { return MenuAction(image: #imageLiteral(resourceName: "ic_trash"), @@ -61,6 +53,14 @@ struct MessageActionBuilder { block: { _ in conversationViewItem.deleteAction() } ) } + + static func banUser(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction { + return MenuAction(image: #imageLiteral(resourceName: "ic_block"), + title: "Ban User", + subtitle: nil, + block: { [weak delegate] _ in delegate?.banUser(conversationViewItem) } + ) + } static func copyMedia(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction { return MenuAction(image: #imageLiteral(resourceName: "ic_copy"), @@ -108,10 +108,9 @@ class ConversationViewItemActions: NSObject { actions.append(deleteAction) } - if isGroup && conversationViewItem.interaction.thread.name() == "Loki Public Chat" - || conversationViewItem.interaction.thread.name() == "Session Public Chat" { - let reportAction = MessageActionBuilder.report(conversationViewItem, delegate: delegate) - actions.append(reportAction) + if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission { + let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate) + actions.append(banAction) } let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate) @@ -152,10 +151,9 @@ class ConversationViewItemActions: NSObject { actions.append(deleteAction) } - if isGroup && conversationViewItem.interaction.thread.name() == "Loki Public Chat" - || conversationViewItem.interaction.thread.name() == "Session Public Chat" { - let reportAction = MessageActionBuilder.report(conversationViewItem, delegate: delegate) - actions.append(reportAction) + if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission { + let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate) + actions.append(banAction) } let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate) @@ -185,10 +183,9 @@ class ConversationViewItemActions: NSObject { actions.append(deleteAction) } - if isGroup && conversationViewItem.interaction.thread.name() == "Loki Public Chat" - || conversationViewItem.interaction.thread.name() == "Session Public Chat" { - let reportAction = MessageActionBuilder.report(conversationViewItem, delegate: delegate) - actions.append(reportAction) + if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission { + let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate) + actions.append(banAction) } let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate) diff --git a/Session/Conversations/Views & Cells/LinkPreviewView.swift b/Session/Conversations/Views & Cells/LinkPreviewView.swift index d8b3b0abe..1b765fb0f 100644 --- a/Session/Conversations/Views & Cells/LinkPreviewView.swift +++ b/Session/Conversations/Views & Cells/LinkPreviewView.swift @@ -2,7 +2,7 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -public extension CGPoint { +extension CGPoint { public func offsetBy(dx: CGFloat) -> CGPoint { return CGPoint(x: x + dx, y: y) } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index ccbaee39e..04994c47e 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -1,30 +1,24 @@ -final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate { - private var threadViewModelCache: [String:ThreadViewModel] = [:] - private var isObservingDatabase = true - private var isViewVisible = false { didSet { updateIsObservingDatabase() } } +// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and +// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for +// more information on database handling. + +final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate { + private var threads: YapDatabaseViewMappings! + private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var tableViewTopConstraint: NSLayoutConstraint! - private var wasDatabaseModifiedExternally = false - private var threads: YapDatabaseViewMappings = { - let result = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) - result.setIsReversed(true, forGroup: TSInboxGroup) - return result - }() + private var threadCount: UInt { + threads.numberOfItems(inGroup: TSInboxGroup) + } - private let uiDatabaseConnection: YapDatabaseConnection = { + private lazy var dbConnection: YapDatabaseConnection = { let result = OWSPrimaryStorage.shared().newDatabaseConnection() result.objectCacheLimit = 500 return result }() - private let editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection() - - private var threadCount: UInt { - threads.numberOfItems(inGroup: TSInboxGroup) - } - - // MARK: Components + // MARK: UI Components private lazy var seedReminderView: SeedReminderView = { let result = SeedReminderView(hasContinueButton: true) let title = "You're almost finished! 80%" @@ -36,9 +30,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol result.delegate = self return result }() - - private lazy var searchBar = SearchBar() - + private lazy var tableView: UITableView = { let result = UITableView() result.backgroundColor = .clear @@ -86,23 +78,26 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() + // Threads (part 1) + dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) + // Preparation SignalApp.shared().homeViewController = self + // Gradient & nav bar setUpGradientBackground() if navigationController?.navigationBar != nil { setUpNavBarStyle() } - updateNavigationBarButtons() + updateNavBarButtons() setNavBarTitle("Messages") - // Set up seed reminder view if needed - let userDefaults = UserDefaults.standard - let hasViewedSeed = userDefaults[.hasViewedSeed] + // Recovery phrase reminder + let hasViewedSeed = UserDefaults.standard[.hasViewedSeed] if !hasViewedSeed { view.addSubview(seedReminderView) seedReminderView.pin(.leading, to: .leading, of: view) seedReminderView.pin(.top, to: .top, of: view) seedReminderView.pin(.trailing, to: .trailing, of: view) } - // Set up table view + // Table view tableView.dataSource = self tableView.delegate = self view.addSubview(tableView) @@ -120,52 +115,59 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol fadeView.pin(.top, to: .top, of: view, withInset: topInset) fadeView.pin(.trailing, to: .trailing, of: view) fadeView.pin(.bottom, to: .bottom, of: view) - // Set up empty state view + // Empty state view view.addSubview(emptyStateView) emptyStateView.center(.horizontal, in: view) let verticalCenteringConstraint = emptyStateView.center(.vertical, in: view) verticalCenteringConstraint.constant = -16 // Makes things appear centered visually - // Set up new conversation button set + // New conversation button set view.addSubview(newConversationButtonSet) newConversationButtonSet.center(.horizontal, in: view) newConversationButtonSet.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up - // Set up previewing + // Previewing if traitCollection.forceTouchCapability == .available { registerForPreviewing(with: self, sourceView: tableView) } - // Listen for notifications + // Notifications let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject) - notificationCenter.addObserver(self, selector: #selector(handleApplicationDidBecomeActiveNotification(_:)), name: .OWSApplicationDidBecomeActive, object: nil) - notificationCenter.addObserver(self, selector: #selector(handleApplicationWillResignActiveNotification(_:)), name: .OWSApplicationWillResignActive, object: nil) notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil) notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil) notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil) notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil) - // Set up public chats and RSS feeds if needed + // Threads (part 2) + threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point + threads.setIsReversed(true, forGroup: TSInboxGroup) + dbConnection.read { transaction in + self.threads.update(with: transaction) // Perform the initial update + } + // Pollers if OWSIdentityManager.shared().identityKeyPair() != nil { let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.startPollerIfNeeded() appDelegate.startClosedGroupPollerIfNeeded() appDelegate.startOpenGroupPollersIfNeeded() } - // Populate onion request path countries cache + // Onion request path countries cache DispatchQueue.global(qos: .utility).async { let _ = IP2Country.shared.populateCacheIfNeeded() } - // Do initial update - reload() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - isViewVisible = true + reload() UserDefaults.standard[.hasLaunchedOnce] = true - showKeyPairMigrationNudgeIfNeeded() + showKeyPairMigrationModalIfNeeded() showKeyPairMigrationSuccessModalIfNeeded() } - private func showKeyPairMigrationNudgeIfNeeded() { + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Migration + private func showKeyPairMigrationModalIfNeeded() { guard !KeyPairUtilities.hasV2KeyPair() else { return } let sheet = KeyPairMigrationSheet() sheet.modalPresentationStyle = .overFullScreen @@ -183,16 +185,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol UserDefaults.standard[.isMigratingToV2KeyPair] = false } - override func viewWillDisappear(_ animated: Bool) { - isViewVisible = false - super.viewWillDisappear(animated) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: Data + // MARK: Table View Data Source func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Int(threadCount) } @@ -204,44 +197,29 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol } // MARK: Updating - private func updateIsObservingDatabase() { - isObservingDatabase = isViewVisible && CurrentAppContext().isAppForegroundAndActive() - } - private func reload() { AssertIsOnMainThread() - uiDatabaseConnection.beginLongLivedReadTransaction() - uiDatabaseConnection.read { transaction in + dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + dbConnection.read { transaction in self.threads.update(with: transaction) } threadViewModelCache.removeAll() tableView.reloadData() emptyStateView.isHidden = (threadCount != 0) } - - @objc private func handleYapDatabaseModifiedNotification(_ notification: Notification) { + + @objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) { AssertIsOnMainThread() - let notifications = uiDatabaseConnection.beginLongLivedReadTransaction() - let ext = uiDatabaseConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection + let notifications = dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + guard !notifications.isEmpty else { return } + let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications) - guard isObservingDatabase else { - wasDatabaseModifiedExternally = hasChanges - return - } - guard hasChanges else { - uiDatabaseConnection.read { transaction in - self.threads.update(with: transaction) - } - return - } - // If changes were made in a different process (e.g. the Notification Service Extension) the thread mapping can be out of date - // at this point, causing the app to crash. The code below prevents that by force syncing the database before proceeding. - if notifications.count > 0 { - if let firstChangeSet = notifications[0].userInfo { - let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64 - if threads.snapshotOfLastUpdate != firstSnapshot - 1 { - return reload() - } + guard hasChanges else { return } + guard !notifications.isEmpty else { return } + if let firstChangeSet = notifications[0].userInfo { + let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64 + if threads.snapshotOfLastUpdate != firstSnapshot - 1 { + return reload() // The code below will crash if we try to process multiple commits at once } } var sectionChanges = NSArray() @@ -254,13 +232,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol let key = rowChange.collectionKey.key threadViewModelCache[key] = nil switch rowChange.type { - case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) - case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade) - case .move: - tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.fade) - tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.fade) - case .update: - tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.none) + case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) + case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic) + case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!) + case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic) default: break } } @@ -268,24 +243,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol emptyStateView.isHidden = (threadCount != 0) } - @objc private func handleApplicationDidBecomeActiveNotification(_ notification: Notification) { - updateIsObservingDatabase() - if wasDatabaseModifiedExternally { - reload() - wasDatabaseModifiedExternally = false - } - } - - @objc private func handleApplicationWillResignActiveNotification(_ notification: Notification) { - updateIsObservingDatabase() - } - @objc private func handleProfileDidChangeNotification(_ notification: Notification) { tableView.reloadData() // TODO: Just reload the affected cell } @objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) { - updateNavigationBarButtons() + updateNavBarButtons() } @objc private func handleSeedViewedNotification(_ notification: Notification) { @@ -298,7 +261,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol self.tableView.reloadData() // TODO: Just reload the affected cell } - private func updateNavigationBarButtons() { + private func updateNavBarButtons() { let profilePictureSize = Values.verySmallProfilePictureSize let profilePictureView = ProfilePictureView() profilePictureView.accessibilityLabel = "Settings button" @@ -353,10 +316,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol present(navigationController, animated: true, completion: nil) } - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - searchBar.resignFirstResponder() - } - func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { guard let indexPath = tableView.indexPathForRow(at: location), let thread = self.thread(at: indexPath.row) else { return nil } previewingContext.sourceRect = tableView.rectForRow(at: indexPath) @@ -465,8 +424,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol } @objc func joinOpenGroup() { - let joinPublicChatVC = JoinPublicChatVC() - let navigationController = OWSNavigationController(rootViewController: joinPublicChatVC) + let joinOpenGroupVC = JoinPublicChatVC() + let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC) present(navigationController, animated: true, completion: nil) } @@ -485,8 +444,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol // MARK: Convenience private func thread(at index: Int) -> TSThread? { var thread: TSThread? = nil - uiDatabaseConnection.read { transaction in - thread = ((transaction as YapDatabaseReadTransaction).ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction).object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? + dbConnection.read { transaction in + let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction + thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? } return thread } @@ -497,7 +457,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol return cachedThreadViewModel } else { var threadViewModel: ThreadViewModel? = nil - uiDatabaseConnection.read { transaction in + dbConnection.read { transaction in threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) } threadViewModelCache[thread.uniqueId!] = threadViewModel diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index f445273e7..6c0eda63b 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -599,24 +599,6 @@ class PhotoCaptureOutputAdaptee: NSObject, ImageCaptureOutput { } completion() } - - // for legacy (iOS10) devices - func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { - if #available(iOS 11, *) { - owsFailDebug("unexpectedly calling legacy method.") - } - - guard let photoSampleBuffer = photoSampleBuffer else { - owsFailDebug("sampleBuffer was unexpectedly nil") - return - } - - let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(photoSampleBuffer) - DispatchQueue.main.async { - self.delegate?.captureOutputDidFinishProcessing(photoData: data, error: error) - } - completion() - } } } @@ -687,6 +669,7 @@ extension AVCaptureVideoOrientation: CustomStringConvertible { return "AVCaptureVideoOrientation.landscapeRight" case .landscapeLeft: return "AVCaptureVideoOrientation.landscapeLeft" + default: preconditionFailure() } } } @@ -708,6 +691,7 @@ extension UIDeviceOrientation: CustomStringConvertible { return "UIDeviceOrientation.faceUp" case .faceDown: return "UIDeviceOrientation.faceDown" + default: preconditionFailure() } } } @@ -731,6 +715,7 @@ extension UIImage.Orientation: CustomStringConvertible { return "UIImageOrientation.leftMirrored" case .rightMirrored: return "UIImageOrientation.rightMirrored" + default: preconditionFailure() } } } diff --git a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift index 930548282..b7b61333c 100644 --- a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift +++ b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift @@ -330,6 +330,7 @@ class PhotoCaptureViewController: OWSViewController { imageName = "ic_flash_mode_on" case .off: imageName = "ic_flash_mode_off" + default: preconditionFailure() } self.flashModeControl.setImage(imageName: imageName) @@ -520,6 +521,7 @@ class CaptureButton: UIView { self.superview?.layoutIfNeeded() } delegate?.didCancelLongPressCaptureButton(self) + default: break } } } diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 952f58dfe..73bb3c8ac 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -470,6 +470,8 @@ static NSTimeInterval launchStartedAt; // Only mark the app as ready once return; } + + [SNConfiguration performMainSetup]; // TODO: Once "app ready" logic is moved into AppSetup, move this line there. [self.profileManager ensureLocalProfileCached]; diff --git a/Session/Meta/Session-Info.plist b/Session/Meta/Session-Info.plist index 57208fbc2..2943e555a 100644 --- a/Session/Meta/Session-Info.plist +++ b/Session/Meta/Session-Info.plist @@ -65,7 +65,7 @@ NSContactsUsageDescription Signal uses your contacts to find users you know. We do not store your contacts on the server. NSFaceIDUsageDescription - Session's Screen Lock feature uses Face ID. + Session's Screen Lock feature uses Face ID. NSMicrophoneUsageDescription Session needs access to your microphone to record media. NSPhotoLibraryAddUsageDescription diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index e11943c7d..d009479f6 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Session starten"; "vc_create_closed_group_group_name_missing_error" = "Bitte geben Sie einen Gruppennamen ein."; "vc_create_closed_group_group_name_too_long_error" = "Bitte geben Sie einen kürzeren Gruppennamen ein."; -"vc_create_closed_group_too_many_group_members_error" = "Eine geschlossene Gruppe kann maximal 20 Mitglieder haben."; +"vc_create_closed_group_too_many_group_members_error" = "Eine geschlossene Gruppe kann maximal 100 Mitglieder haben."; "vc_create_closed_group_invalid_session_id_error" = "Ein Mitglied Ihrer Gruppe hat eine ungültige Session ID."; "vc_join_public_chat_title" = "Offener Gruppe beitreten"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index c3869fecf..2389da5bf 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -2647,7 +2647,7 @@ "vc_create_closed_group_empty_state_button_title" = "Start a Session"; "vc_create_closed_group_group_name_missing_error" = "Please enter a group name"; "vc_create_closed_group_group_name_too_long_error" = "Please enter a shorter group name"; -"vc_create_closed_group_too_many_group_members_error" = "A closed group cannot have more than 20 members"; +"vc_create_closed_group_too_many_group_members_error" = "A closed group cannot have more than 100 members"; "vc_create_closed_group_invalid_session_id_error" = "One of the members of your group has an invalid Session ID"; "vc_join_public_chat_title" = "Join Open Group"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 660d87848..adb113137 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Empezar una Session"; "vc_create_closed_group_group_name_missing_error" = "Por favor, ingresa un nombre de grupo"; "vc_create_closed_group_group_name_too_long_error" = "Por favor, ingresa un nombre de grupo más corto"; -"vc_create_closed_group_too_many_group_members_error" = "Un grupo cerrado no puede tener más de 20 miembros"; +"vc_create_closed_group_too_many_group_members_error" = "Un grupo cerrado no puede tener más de 100 miembros"; "vc_create_closed_group_invalid_session_id_error" = "Uno de los miembros de tu grupo tiene un ID de Session no válido"; "vc_join_public_chat_title" = "Únete al grupo abierto"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index ae1bfa0b0..6990b5dfe 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "شروع Session"; "vc_create_closed_group_group_name_missing_error" = "لطفا یک نام گروه وارد کنید"; "vc_create_closed_group_group_name_too_long_error" = "لطفا نام گروه کوتاه‌تری وارد کنید"; -"vc_create_closed_group_too_many_group_members_error" = "یک گروه خصوصی نمی‌تواند بیش از بیست عضو داشته باشد"; +"vc_create_closed_group_too_many_group_members_error" = "یک گروه خصوصی نمی‌تواند بیش از یکصد عضو داشته باشد"; "vc_create_closed_group_invalid_session_id_error" = "یکی از اعضای گروه شما دارای شناسه نامعتبر است"; "vc_join_public_chat_title" = "به گروه باز بپیوندید"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 5e8b2759b..7324948a0 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -2648,7 +2648,7 @@ "vc_create_closed_group_empty_state_button_title" = "Démarrer une session"; "vc_create_closed_group_group_name_missing_error" = "Veuillez saisir un nom de groupe"; "vc_create_closed_group_group_name_too_long_error" = "Veuillez saisir un nom de groupe plus court"; -"vc_create_closed_group_too_many_group_members_error" = "Un groupe privé ne peut pas avoir plus de 20 membres"; +"vc_create_closed_group_too_many_group_members_error" = "Un groupe privé ne peut pas avoir plus de 100 membres"; "vc_create_closed_group_invalid_session_id_error" = "Un des membres de votre groupe a un Session ID non valide"; "vc_join_public_chat_title" = "Joindre un groupe public"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 2a90f7762..143e2a57b 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -2639,7 +2639,7 @@ "vc_create_closed_group_group_name_missing_error" = "Masukkan nama grup"; "vc_create_closed_group_group_name_too_long_error" = "Masukkan nama grup yang lebih pendek"; "vc_create_closed_group_not_enough_group_members_error" = "Pilih setidaknya 2 anggota grup"; -"vc_create_closed_group_too_many_group_members_error" = "Grup tertutup maksimal berisi 20 anggota"; +"vc_create_closed_group_too_many_group_members_error" = "Grup tertutup maksimal berisi 100 anggota"; "vc_create_closed_group_invalid_session_id_error" = "Salah satu anggota di grup memiliki Session ID yang salah"; "vc_join_public_chat_title" = "Gabung ke grup terbuka"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index a76836cdf..7e69c6748 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Inizia una sessione"; "vc_create_closed_group_group_name_missing_error" = "Inserisci un nome per il gruppo"; "vc_create_closed_group_group_name_too_long_error" = "Inserisci un nome gruppo più breve"; -"vc_create_closed_group_too_many_group_members_error" = "Un gruppo chiuso non può avere più di 20 membri"; +"vc_create_closed_group_too_many_group_members_error" = "Un gruppo chiuso non può avere più di 100 membri"; "vc_create_closed_group_invalid_session_id_error" = "Uno dei membri del tuo gruppo ha una Sessione ID non valido"; "vc_join_public_chat_title" = "Unisciti a un gruppo aperto"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index a6fca3227..aa65f4d9f 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -2639,7 +2639,7 @@ "vc_create_closed_group_group_name_missing_error" = "グループ名を入力してください"; "vc_create_closed_group_group_name_too_long_error" = "短いグループ名を入力してください"; "vc_create_closed_group_not_enough_group_members_error" = "グループメンバーを少なくとも 2 人選択してください"; -"vc_create_closed_group_too_many_group_members_error" = "閉じたグループは 20 人を超えるメンバーを抱えることはできません"; +"vc_create_closed_group_too_many_group_members_error" = "閉じたグループは 100 人を超えるメンバーを抱えることはできません"; "vc_create_closed_group_invalid_session_id_error" = "グループのメンバーの 1 人の Session ID が無効です"; "vc_join_public_chat_title" = "オープングループに参加する"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index ac83c445a..c82df30af 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Rozpocznij sesję"; "vc_create_closed_group_group_name_missing_error" = "Wpisz nazwę grupy"; "vc_create_closed_group_group_name_too_long_error" = "Wprowadź krótszą nazwę grupy"; -"vc_create_closed_group_too_many_group_members_error" = "Grupa zamknięta nie może mieć więcej niż 20 członków"; +"vc_create_closed_group_too_many_group_members_error" = "Grupa zamknięta nie może mieć więcej niż 100 członków"; "vc_create_closed_group_invalid_session_id_error" = "Jeden z członków Twojej grupy ma nieprawidłowy identyfikator Session"; "vc_join_public_chat_title" = "Dołącz do Open Group"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 1c5489cb3..a6de3c8aa 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Iniciar uma sessão"; "vc_create_closed_group_group_name_missing_error" = "Digite um nome de grupo"; "vc_create_closed_group_group_name_too_long_error" = "Digite um nome de grupo mais curto"; -"vc_create_closed_group_too_many_group_members_error" = "Um grupo fechado não pode ter mais de 20 membros"; +"vc_create_closed_group_too_many_group_members_error" = "Um grupo fechado não pode ter mais de 100 membros"; "vc_create_closed_group_invalid_session_id_error" = "Um dos membros do seu grupo tem um ID Session inválido"; "vc_join_public_chat_title" = "Participar em grupo aberto"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 8d8a1f90f..e3400b41a 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "Начать Сессию"; "vc_create_closed_group_group_name_missing_error" = "Пожалуйста, введите название группы"; "vc_create_closed_group_group_name_too_long_error" = "Пожалуйста, введите более короткое имя группы"; -"vc_create_closed_group_too_many_group_members_error" = "В закрытой группе не может быть больше 20 участников"; +"vc_create_closed_group_too_many_group_members_error" = "В закрытой группе не может быть больше 100 участников"; "vc_create_closed_group_invalid_session_id_error" = "Один из участников вашей группы имеет недопустимый Session ID"; "vc_join_public_chat_title" = "Присоединиться к открытой группе"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index e5765a93c..5ec4fbbc9 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -2648,7 +2648,7 @@ "vc_create_closed_group_group_name_missing_error" = "Vui lòng nhập tên nhóm"; "vc_create_closed_group_group_name_too_long_error" = "Vui lòng nhập một tên nhóm ngắn hơn "; "vc_create_closed_group_not_enough_group_members_error" = "Vui lòng chọn ít nhất 2 thành viên trong nhóm "; -"vc_create_closed_group_too_many_group_members_error" = "Một nhóm kín không thể có nhiều hơn 20 thành viên "; +"vc_create_closed_group_too_many_group_members_error" = "Một nhóm kín không thể có nhiều hơn 100 thành viên "; "vc_create_closed_group_invalid_session_id_error" = "Một trong các thành viên trong nhóm của bạn có Session ID không hợp lệ "; "vc_join_public_chat_title" = "Tham gia nhóm mở"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index e07e05837..5bb9df603 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -2638,7 +2638,7 @@ "vc_create_closed_group_empty_state_button_title" = "开始对话"; "vc_create_closed_group_group_name_missing_error" = "请输入群组名称"; "vc_create_closed_group_group_name_too_long_error" = "请输入较短的群组名称"; -"vc_create_closed_group_too_many_group_members_error" = "私密群组成员不得超过20个"; +"vc_create_closed_group_too_many_group_members_error" = "私密群组成员不得超过100个"; "vc_create_closed_group_invalid_session_id_error" = "您群组中的一位成员的Session ID无效"; "vc_join_public_chat_title" = "加入公开群组"; diff --git a/Session/Utilities/AppUpdateNag.swift b/Session/Utilities/AppUpdateNag.swift index 6694966ec..eee81cde8 100644 --- a/Session/Utilities/AppUpdateNag.swift +++ b/Session/Utilities/AppUpdateNag.swift @@ -21,6 +21,7 @@ class AppUpdateNag: NSObject { public func showAppUpgradeNagIfNecessary() { return + /* guard let currentVersion = self.currentVersion else { owsFailDebug("currentVersion was unexpectedly nil") return @@ -49,6 +50,7 @@ class AppUpdateNag: NSObject { }.catch { error in Logger.error("failed with error: \(error)") }.retainUntilComplete() + */ } // MARK: - Internal @@ -110,7 +112,7 @@ class AppUpdateNag: NSObject { // Only show nag if we are "at rest" in the home view or registration view without any // alerts or dialogs showing. - guard let frontmostViewController = UIApplication.shared.frontmostViewController else { + guard UIApplication.shared.frontmostViewController != nil else { owsFailDebug("frontmostViewController was unexpectedly nil") return } diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index 7797c3ee5..d84c43c72 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -180,14 +180,18 @@ extension Storage { (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: openGroupID, inCollection: Storage.openGroupUserCountCollection) } - public func getIDForMessage(withServerID serverID: UInt64) -> UInt64? { - var result: UInt64? = nil + public func getIDForMessage(withServerID serverID: UInt64) -> String? { + var result: String? = nil Storage.read { transaction in - result = transaction.object(forKey: String(serverID), inCollection: Storage.openGroupMessageIDCollection) as? UInt64 + result = transaction.object(forKey: String(serverID), inCollection: Storage.openGroupMessageIDCollection) as? String } return result } + public func setIDForMessage(withServerID serverID: UInt64, to messageID: String, using transaction: Any) { + (transaction as! YapDatabaseReadWriteTransaction).setObject(messageID, forKey: String(serverID), inCollection: Storage.openGroupMessageIDCollection) + } + public func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any) { let collection = openGroupID (transaction as! YapDatabaseReadWriteTransaction).setObject(displayName, forKey: publicKey, inCollection: collection) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index ad0f2c63b..d6d39e854 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -218,13 +218,13 @@ public final class OpenGroupAPI : DotNetAPI { @objc(deleteMessageWithID:forGroup:onServer:isSentByUser:) public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise { - return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, isSentByUser: isSentByUser)) + return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, wasSentByUser: isSentByUser)) } - public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise { - let isModerationRequest = !isSentByUser + public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, wasSentByUser: Bool) -> Promise { + let isModerationRequest = !wasSentByUser SNLog("Deleting message with ID: \(messageID) for open group channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).") - let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" + let urlAsString = wasSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in @@ -238,6 +238,33 @@ public final class OpenGroupAPI : DotNetAPI { }.handlingInvalidAuthTokenIfNeeded(for: server) } } + + // MARK: Banning + @objc(banPublicKey:fromServer:) + public static func objc_ban(_ publicKey: String, from server: String) -> AnyPromise { + return AnyPromise.from(ban(publicKey, from: server)) + } + + public static func ban(_ publicKey: String, from server: String) -> Promise { + SNLog("Banning user with ID: \(publicKey) from server: \(server).") + return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { + getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + let url = URL(string: "\(server)/loki/v1/moderation/blacklist/@\(publicKey)")! + let request = TSRequest(url: url, method: "POST", parameters: [:]) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false) + promise.done(on: DispatchQueue.global(qos: .default)) { _ -> Void in + SNLog("Banned user with ID: \(publicKey) from server: \(server).") + } + promise.catch(on: DispatchQueue.main) { error in + print(error) + } + return promise.map { _ in } + } + }.handlingInvalidAuthTokenIfNeeded(for: server) + } + } // MARK: Display Name & Profile Picture public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index a0a4f0de8..27b841071 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -17,6 +17,12 @@ extension MessageReceiver { case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) default: fatalError() } + // Touch the thread to update the home screen preview + let storage = SNMessagingKitConfiguration.shared.storage + guard let threadID = storage.getOrCreateThread(for: message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } + thread.touch(with: transaction) } private static func handleReadReceipt(_ message: ReadReceipt, using transaction: Any) { @@ -226,6 +232,10 @@ extension MessageReceiver { if isMainAppAndActive { cancelTypingIndicatorsIfNeeded(for: message.sender!) } + // Keep track of the open group server message ID ↔ message ID relationship + if let serverID = message.openGroupServerMessageID { + storage.setIDForMessage(withServerID: serverID, to: tsIncomingMessageID, using: transaction) + } // Notify the user if needed guard (isMainAppAndActive || isBackgroundPoll), let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID } @@ -289,7 +299,7 @@ extension MessageReceiver { let group = thread.groupModel let oldMembers = group.groupMemberIds // Check that the message isn't from before the group was created - guard Double(message.sentTimestamp!) > thread.creationDate.timeIntervalSince1970 else { + guard Double(message.sentTimestamp!) > thread.creationDate.timeIntervalSince1970 * 1000 else { return SNLog("Ignoring closed group update from before thread was created.") } // Check that the sender is a member of the group (before the update) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 7fbe5e155..b2fd2d58a 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -159,7 +159,7 @@ public final class MessageSender : NSObject { // Serialize the protobuf let plaintext: Data do { - plaintext = try proto.serializedData() + plaintext = (try proto.serializedData() as NSData).paddedMessageBody() } catch { SNLog("Couldn't serialize proto due to error: \(error).") handleFailure(with: error, using: transaction) @@ -343,15 +343,21 @@ public final class MessageSender : NSObject { Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction) // To later ignore self-sends in a multi device context guard let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) else { return } tsMessage.openGroupServerMessageID = message.openGroupServerMessageID ?? 0 + let storage = SNMessagingKitConfiguration.shared.storage + let transaction = transaction as! YapDatabaseReadWriteTransaction + tsMessage.save(with: transaction) + if let serverID = message.openGroupServerMessageID { + storage.setIDForMessage(withServerID: serverID, to: tsMessage.uniqueId!, using: transaction) + } var recipients = [ message.recipient! ] if case .closedGroup(_) = destination, let threadID = message.threadID, // threadID should always be set at this point - let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction as! YapDatabaseReadTransaction), thread.isClosedGroup { + let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction), thread.isClosedGroup { recipients = thread.groupModel.groupMemberIds } recipients.forEach { recipient in - tsMessage.update(withSentRecipient: recipient, wasSentByUD: true, transaction: transaction as! YapDatabaseReadWriteTransaction) + tsMessage.update(withSentRecipient: recipient, wasSentByUD: true, transaction: transaction) } - OWSDisappearingMessagesJob.shared().startAnyExpiration(for: tsMessage, expirationStartedAt: NSDate.millisecondTimestamp(), transaction: transaction as! YapDatabaseReadWriteTransaction) + OWSDisappearingMessagesJob.shared().startAnyExpiration(for: tsMessage, expirationStartedAt: NSDate.millisecondTimestamp(), transaction: transaction) // Sync the message if: // • it wasn't a self-send // • it was a visible message diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 79486d488..bd8f7d243 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -19,7 +19,7 @@ public final class OpenGroupPoller : NSObject { // MARK: Settings private let pollForNewMessagesInterval: TimeInterval = 4 - private let pollForDeletedMessagesInterval: TimeInterval = 60 + private let pollForDeletedMessagesInterval: TimeInterval = 30 private let pollForModeratorsInterval: TimeInterval = 10 * 60 // MARK: Lifecycle @@ -193,9 +193,10 @@ public final class OpenGroupPoller : NSObject { let openGroup = self.openGroup let _ = OpenGroupAPI.getDeletedMessageServerIDs(for: openGroup.channel, on: openGroup.server).done(on: DispatchQueue.global(qos: .default)) { deletedMessageServerIDs in let deletedMessageIDs = deletedMessageServerIDs.compactMap { Storage.shared.getIDForMessage(withServerID: UInt64($0)) } - SNMessagingKitConfiguration.shared.storage.writeSync { transaction in + SNMessagingKitConfiguration.shared.storage.write { transaction in deletedMessageIDs.forEach { messageID in - TSMessage.fetch(uniqueId: String(messageID))?.remove(with: transaction as! YapDatabaseReadWriteTransaction) + let transaction = transaction as! YapDatabaseReadWriteTransaction + TSMessage.fetch(uniqueId: messageID, transaction: transaction)?.remove(with: transaction) } } } diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index c35bb3d13..a835578fe 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -74,7 +74,8 @@ public protocol SessionMessagingKitStorageProtocol { // MARK: - Open Group Metadata func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any) - func getIDForMessage(withServerID serverID: UInt64) -> UInt64? + func getIDForMessage(withServerID serverID: UInt64) -> String? + func setIDForMessage(withServerID serverID: UInt64, to messageID: String, using transaction: Any) func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any) func setLastProfilePictureUploadDate(_ date: Date) // Stored in user defaults so no transaction is needed diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 60de986f7..7676b511e 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -2,8 +2,6 @@ import UserNotifications import SessionMessagingKit import SignalUtilitiesKit -// TODO: Group notifications - public final class NotificationServiceExtension : UNNotificationServiceExtension { private var didPerformSetup = false private var areVersionMigrationsComplete = false diff --git a/SessionShareExtension/ShareViewController.swift b/SessionShareExtension/ShareViewController.swift index 6383cf785..5c1625ffa 100644 --- a/SessionShareExtension/ShareViewController.swift +++ b/SessionShareExtension/ShareViewController.swift @@ -180,7 +180,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // Avoid blocking app launch by putting all further possible DB access in async block DispatchQueue.global().async { [weak self] in guard let _ = self else { return } - Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber)") + Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber())") // We don't need to use OWSDisappearingMessagesJob in the SAE. @@ -199,7 +199,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed if tsAccountManager.isRegistered() { DispatchQueue.main.async { [weak self] in guard let _ = self else { return } - Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber)") + Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber())") // We don't need to use the TSSocketManager in the SAE. @@ -258,7 +258,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed AppReadiness.setAppIsReady() if tsAccountManager.isRegistered() { - Logger.info("localNumber: \(TSAccountManager.localNumber)") + Logger.info("localNumber: \(TSAccountManager.localNumber())") // We don't need to use messageFetcherJob in the SAE. @@ -290,7 +290,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.debug("") if tsAccountManager.isRegistered() { - Logger.info("localNumber: \(TSAccountManager.localNumber)") + Logger.info("localNumber: \(TSAccountManager.localNumber())") // We don't need to use ExperienceUpgradeFinder in the SAE. @@ -659,12 +659,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed var visualMediaItemProviders = [NSItemProvider]() var hasNonVisualMedia = false for attachment in attachments { - guard let itemProvider = attachment as? NSItemProvider else { - owsFailDebug("Unexpected attachment type: \(String(describing: attachment))") - continue - } - if isVisualMediaItem(itemProvider: itemProvider) { - visualMediaItemProviders.append(itemProvider) + if isVisualMediaItem(itemProvider: attachment) { + visualMediaItemProviders.append(attachment) } else { hasNonVisualMedia = true } @@ -690,15 +686,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } return isUrlItem(itemProvider: itemProvider) }) { - if let itemProvider = preferredAttachment as? NSItemProvider { - return [itemProvider] - } else { - owsFailDebug("Unexpected attachment type: \(String(describing: preferredAttachment))") - } + return [preferredAttachment] } // else return whatever is available - if let itemProvider = inputItem.attachments?.first as? NSItemProvider { + if let itemProvider = inputItem.attachments?.first { return [itemProvider] } else { owsFailDebug("Missing attachment.") diff --git a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h index 23acc2c83..670ac0efe 100644 --- a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h +++ b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h @@ -10,22 +10,38 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; #import #import +#import #import +#import +#import +#import #import +#import +#import #import +#import +#import #import #import #import #import +#import #import #import +#import +#import #import #import #import #import +#import #import #import +#import +#import +#import #import +#import #import #import #import @@ -37,7 +53,10 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; #import #import #import +#import #import +#import +#import #import #import #import diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index 0ee8d9088..b9dc3fe64 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -4,14 +4,15 @@ import Foundation import MediaPlayer - +import SessionUIKit // A modal view that be used during blocking interactions (e.g. waiting on response from // service or on the completion of a long-running local operation). @objc public class ModalActivityIndicatorViewController: OWSViewController { - let canCancel: Bool + + let message: String? @objc public var wasCancelled: Bool = false @@ -29,25 +30,26 @@ public class ModalActivityIndicatorViewController: OWSViewController { notImplemented() } - public required init(canCancel: Bool) { + public required init(canCancel: Bool = false, message: String? = nil) { self.canCancel = canCancel + self.message = message super.init(nibName: nil, bundle: nil) } @objc - public class func present(fromViewController: UIViewController, - canCancel: Bool, backgroundBlock : @escaping (ModalActivityIndicatorViewController) -> Void) { + public class func present(fromViewController: UIViewController, canCancel: Bool = false, message: String? = nil, + backgroundBlock : @escaping (ModalActivityIndicatorViewController) -> Void) { AssertIsOnMainThread() - let view = ModalActivityIndicatorViewController(canCancel: canCancel) + let view = ModalActivityIndicatorViewController(canCancel: canCancel, message: message) // Present this modal _over_ the current view contents. view.modalPresentationStyle = .overFullScreen view.modalTransitionStyle = .crossDissolve - fromViewController.present(view, - animated: false) { - DispatchQueue.global().async { - backgroundBlock(view) - } + fromViewController.present(view, animated: false) { + DispatchQueue.global().async { + backgroundBlock(view) + + } } } @@ -70,15 +72,31 @@ public class ModalActivityIndicatorViewController: OWSViewController { public override func loadView() { super.loadView() - self.view.backgroundColor = (Theme.isDarkThemeEnabled - ? UIColor(white: 0, alpha: 0.5) - : UIColor(white: 0, alpha: 0.5)) + self.view.backgroundColor = UIColor(white: 0, alpha: 0.5) self.view.isOpaque = false let activityIndicator = UIActivityIndicatorView(style: .whiteLarge) self.activityIndicator = activityIndicator - self.view.addSubview(activityIndicator) - activityIndicator.autoCenterInSuperview() + + if let message = message { + let messageLabel = UILabel() + messageLabel.text = message + messageLabel.font = .systemFont(ofSize: Values.mediumFontSize) + messageLabel.textColor = UIColor.white + messageLabel.numberOfLines = 0 + messageLabel.textAlignment = .center + messageLabel.lineBreakMode = .byWordWrapping + messageLabel.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) + let stackView = UIStackView(arrangedSubviews: [ messageLabel, activityIndicator ]) + stackView.axis = .vertical + stackView.spacing = Values.largeSpacing + stackView.alignment = .center + self.view.addSubview(stackView) + stackView.center(in: self.view) + } else { + self.view.addSubview(activityIndicator) + activityIndicator.autoCenterInSuperview() + } if canCancel { let cancelButton = UIButton(type: .custom) @@ -152,7 +170,6 @@ public class ModalActivityIndicatorViewController: OWSViewController { wasCancelled = true - dismiss { - } + dismiss { } } }