Ditch unused Signal code
This commit is contained in:
parent
14c87139c6
commit
f36f447bec
|
@ -50,11 +50,11 @@ final class ConversationTitleView : UIView {
|
|||
updateSubtitleForCurrentStatus()
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleRoutingNotification(_:)), name: .routing, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingMessagePoWNotification(_:)), name: .calculatingMessagePoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleEncryptingMessageNotification(_:)), name: .encryptingMessage, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingFailedNotification(_:)), name: .messageSendingFailed, object: nil)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -112,12 +112,12 @@ final class ConversationTitleView : UIView {
|
|||
updateProfilePicture()
|
||||
}
|
||||
|
||||
@objc private func handleCalculatingPoWNotification(_ notification: Notification) {
|
||||
@objc private func handleCalculatingMessagePoWNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleRoutingNotification(_ notification: Notification) {
|
||||
@objc private func handleEncryptingMessageNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleMessageFailedNotification(_ notification: Notification) {
|
||||
@objc private func handleMessageSendingFailedNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
clearStatusIfNeededForMessageWithTimestamp(timestamp)
|
||||
}
|
||||
|
@ -149,14 +149,7 @@ final class ConversationTitleView : UIView {
|
|||
uncheckedTargetInteraction = interaction
|
||||
}
|
||||
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage,
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0), let hexEncodedPublicKey = targetInteraction.thread.contactIdentifier() else { return }
|
||||
var masterHexEncodedPublicKey: String!
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
|
||||
}
|
||||
let isSlaveDevice = masterHexEncodedPublicKey != hexEncodedPublicKey
|
||||
guard !isSlaveDevice else { return }
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0) else { return }
|
||||
currentStatus = status
|
||||
}
|
||||
|
||||
|
@ -184,7 +177,7 @@ final class ConversationTitleView : UIView {
|
|||
dateFormatter.timeStyle = .medium
|
||||
dateFormatter.dateStyle = .medium
|
||||
subtitle.append(NSAttributedString(string: "Muted until " + dateFormatter.string(from: muteEndDate)))
|
||||
} else if let thread = self.thread as? TSGroupThread, !thread.isRSSFeed {
|
||||
} else if let thread = self.thread as? TSGroupThread {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
var userCount: Int?
|
||||
if thread.groupModel.groupType == .closedGroup {
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
#import "ConversationViewCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "DateUtil.h"
|
||||
#import "FingerprintViewController.h"
|
||||
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "NotificationSettingsViewController.h"
|
||||
#import "OWSAddToContactViewController.h"
|
||||
|
||||
#import "OWSAnyTouchGestureRecognizer.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import "OWSBackup.h"
|
||||
|
@ -35,13 +35,12 @@
|
|||
#import "OWSQuotedMessageView.h"
|
||||
#import "OWSSessionResetJobRecord.h"
|
||||
#import "OWSWindowManager.h"
|
||||
#import "PinEntryView.h"
|
||||
#import "PrivacySettingsTableViewController.h"
|
||||
#import "RemoteVideoView.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "SignalApp.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
|
||||
#import <SessionProtocolKit/NSData+keyVersionByte.h>
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
|
@ -55,52 +54,36 @@
|
|||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSContactAvatarBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/OWSQuotedReplyModel.h>
|
||||
#import <SignalUtilitiesKit/OWSSounds.h>
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIViewController+OWS.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SignalUtilitiesKit/Contact.h>
|
||||
#import <SignalUtilitiesKit/ContactsUpdater.h>
|
||||
#import <SignalUtilitiesKit/DataSource.h>
|
||||
#import <SignalUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalyticsEvents.h>
|
||||
#import <SignalUtilitiesKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSCallMessageHandler.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsOutputStream.h>
|
||||
#import <SignalUtilitiesKit/OWSDispatch.h>
|
||||
#import <SignalUtilitiesKit/OWSEndSessionMessage.h>
|
||||
#import <SignalUtilitiesKit/LKDeviceLinkMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMediaGalleryFinder.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageReceiver.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/OWSOutgoingCallMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileKeyMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSRecipientIdentity.h>
|
||||
#import <SignalUtilitiesKit/OWSRequestFactory.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/SignalRecipient.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
|
@ -113,10 +96,10 @@
|
|||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSIncomingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
#import <SignalUtilitiesKit/TSNetworkManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/LKGroupUtilities.h>
|
||||
#import <SignalUtilitiesKit/UIImage+OWS.h>
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
#import <SignalCoreKit/NSObject+OWS.h>
|
||||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalUtilitiesKit/SSKAsserts.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSSet+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSObject+Casting.h>
|
||||
|
|
|
@ -19,10 +19,6 @@ public class AccountManager: NSObject {
|
|||
return OWSProfileManager.shared()
|
||||
}
|
||||
|
||||
private var networkManager: TSNetworkManager {
|
||||
return SSKEnvironment.shared.networkManager
|
||||
}
|
||||
|
||||
private var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
@ -119,28 +115,4 @@ public class AccountManager: NSObject {
|
|||
let anyPromise = tsAccountManager.setIsManualMessageFetchEnabled(true)
|
||||
return Promise(anyPromise).asVoid()
|
||||
}
|
||||
|
||||
// MARK: Turn Server
|
||||
|
||||
func getTurnServerInfo() -> Promise<TurnServerInfo> {
|
||||
return Promise { resolver in
|
||||
self.networkManager.makeRequest(OWSRequestFactory.turnServerInfoRequest(),
|
||||
success: { (_: URLSessionDataTask, responseObject: Any?) in
|
||||
guard responseObject != nil else {
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
}
|
||||
|
||||
if let responseDictionary = responseObject as? [String: AnyObject] {
|
||||
if let turnServerInfo = TurnServerInfo(attributes: responseDictionary) {
|
||||
return resolver.fulfill(turnServerInfo)
|
||||
}
|
||||
Logger.error("unexpected server response:\(responseDictionary)")
|
||||
}
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
},
|
||||
failure: { (_: URLSessionDataTask, error: Error) in
|
||||
return resolver.reject(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ContactsUI
|
||||
|
||||
class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPickerDelegate, CNContactViewControllerDelegate {
|
||||
|
||||
// TODO - there are some hard coded assumptions in this VC that assume we are *pushed* onto a
|
||||
// navigation controller. That seems fine for now, but if we need to be presented as a modal,
|
||||
// or need to notify our presenter about our dismisall or other contact actions, a delegate
|
||||
// would be helpful. It seems like this would require some broad changes to the ContactShareViewHelper,
|
||||
// so I've left it as is for now, since it happens to work.
|
||||
// weak var addToExistingContactDelegate: AddContactShareToExistingContactViewControllerDelegate?
|
||||
|
||||
let contactShare: ContactShareViewModel
|
||||
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
self.contactShare = contactShare
|
||||
super.init(allowsMultipleSelection: false, subtitleCellType: .none)
|
||||
|
||||
self.contactsPickerDelegate = self
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
owsFailDebug("with error: \(error)")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact oldContact: Contact) {
|
||||
Logger.debug("")
|
||||
|
||||
let contactsManager = Environment.shared.contactsManager
|
||||
guard let oldCNContact = contactsManager?.cnContact(withId: oldContact.cnContactId) else {
|
||||
owsFailDebug("could not load old CNContact.")
|
||||
return
|
||||
}
|
||||
guard let newCNContact = OWSContacts.systemContact(for: self.contactShare.dbRecord, imageData: self.contactShare.avatarImageData) else {
|
||||
owsFailDebug("could not load new CNContact.")
|
||||
return
|
||||
}
|
||||
merge(oldCNContact: oldCNContact, newCNContact: newCNContact)
|
||||
}
|
||||
|
||||
func merge(oldCNContact: CNContact, newCNContact: CNContact) {
|
||||
Logger.debug("")
|
||||
|
||||
let mergedCNContact: CNContact = Contact.merge(cnContact: oldCNContact, newCNContact: newCNContact)
|
||||
|
||||
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
|
||||
// saving our users a tap in some cases when we already know they want to edit.
|
||||
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedCNContact)
|
||||
|
||||
// Default title is "New Contact". We could give a more descriptive title, but anything
|
||||
// seems redundant - the context is sufficiently clear.
|
||||
contactViewController.title = ""
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.delegate = self
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
self.present(modal, animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("")
|
||||
owsFailDebug("only supports single contact select")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.debug("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO this is weird - ideally we'd do something like
|
||||
// self.delegate?.didFinishAddingContact
|
||||
// and the delegate, which knows about our presentation context could do the right thing.
|
||||
//
|
||||
// As it is, we happen to always be *pushing* this view controller onto a navcontroller, so the
|
||||
// following works in all current cases.
|
||||
//
|
||||
// If we ever wanted to do something different, like present this in a modal, we'd have to rethink.
|
||||
|
||||
// We want to pop *this* view *and* the still presented CNContactViewController in a single animation.
|
||||
// Note this happens for *cancel* and for *done*. Unfortunately, I don't know of a way to detect the difference
|
||||
// between the two, since both just call this method.
|
||||
guard let myIndex = navigationController.viewControllers.firstIndex(of: self) else {
|
||||
owsFailDebug("myIndex was unexpectedly nil")
|
||||
navigationController.popViewController(animated: true)
|
||||
navigationController.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
let previousViewControllerIndex = navigationController.viewControllers.index(before: myIndex)
|
||||
let previousViewController = navigationController.viewControllers[previousViewControllerIndex]
|
||||
|
||||
self.dismiss(animated: false) {
|
||||
navigationController.popToViewController(previousViewController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -51,8 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
__weak AddToBlockListViewController *weakSelf = self;
|
||||
[BlockListUIUtils showBlockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:self.contactsViewHelper.blockingManager
|
||||
contactsManager:self.contactsViewHelper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (isBlocked) {
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
@ -65,7 +64,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(signalAccount);
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
return ![helper isRecipientIdBlocked:signalAccount.recipientId];
|
||||
return ![SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
|
||||
|
@ -73,15 +72,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak AddToBlockListViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([helper isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
OWSFailDebug(@"Cannot add already blocked user to block list.");
|
||||
return;
|
||||
}
|
||||
[BlockListUIUtils showBlockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (isBlocked) {
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
#import "AddToGroupViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -52,12 +52,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([helper isRecipientIdBlocked:phoneNumber]) {
|
||||
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:phoneNumber]) {
|
||||
[BlockListUIUtils showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
|
@ -66,22 +64,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:phoneNumber
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:phoneNumber];
|
||||
}
|
||||
|
||||
|
@ -97,17 +79,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId]) {
|
||||
OWSFailDebug(@"Cannot add user to group member if already a member.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ([helper isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
[BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
|
@ -116,22 +96,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:signalAccount.recipientId
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSTableViewController.h"
|
||||
|
||||
@interface AdvancedSettingsTableViewController : OWSTableViewController
|
||||
|
||||
@end
|
|
@ -1,312 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AdvancedSettingsTableViewController.h"
|
||||
#import "DebugLogger.h"
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AdvancedSettingsTableViewController ()
|
||||
|
||||
@property (nonatomic) Reachability *reachability;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AdvancedSettingsTableViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_ADVANCED_TITLE", @"");
|
||||
|
||||
self.reachability = [Reachability reachabilityForInternetConnection];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(socketStateDidChange)
|
||||
name:kNSNotification_OWSWebSocketStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(reachabilityChanged)
|
||||
name:kReachabilityChangedNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)socketStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)reachabilityChanged
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak AdvancedSettingsTableViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *loggingSection = [OWSTableSection new];
|
||||
loggingSection.headerTitle = NSLocalizedString(@"LOGGING_SECTION", nil);
|
||||
[loggingSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"enable_debug_log")
|
||||
isOnBlock:^{
|
||||
return [OWSPreferences isLoggingEnabled];
|
||||
}
|
||||
isEnabledBlock:^{
|
||||
return YES;
|
||||
}
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableLogSwitch:)]];
|
||||
|
||||
|
||||
if ([OWSPreferences isLoggingEnabled]) {
|
||||
[loggingSection
|
||||
addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"submit_debug_log")
|
||||
actionBlock:^{
|
||||
OWSLogInfo(@"Submitting debug logs");
|
||||
[DDLog flushLog];
|
||||
[Pastelog submitLogs];
|
||||
}]];
|
||||
}
|
||||
|
||||
[contents addSection:loggingSection];
|
||||
|
||||
OWSTableSection *pushNotificationsSection = [OWSTableSection new];
|
||||
pushNotificationsSection.headerTitle
|
||||
= NSLocalizedString(@"PUSH_REGISTER_TITLE", @"Used in table section header and alert view title contexts");
|
||||
[pushNotificationsSection addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"REREGISTER_FOR_PUSH", nil)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
self, @"reregister_push_notifications")
|
||||
actionBlock:^{
|
||||
[weakSelf syncPushTokens];
|
||||
}]];
|
||||
[contents addSection:pushNotificationsSection];
|
||||
|
||||
// Censorship circumvention has certain disadvantages so it should only be
|
||||
// used if necessary. Therefore:
|
||||
//
|
||||
// * We disable this setting if the user has a phone number from a censored region -
|
||||
// censorship circumvention will be auto-activated for this user.
|
||||
// * We disable this setting if the user is already connected; they're not being
|
||||
// censored.
|
||||
// * We continue to show this setting so long as it is set to allow users to disable
|
||||
// it, for example when they leave a censored region.
|
||||
OWSTableSection *censorshipSection = [OWSTableSection new];
|
||||
censorshipSection.headerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_HEADER",
|
||||
@"Table header for the 'censorship circumvention' section.");
|
||||
BOOL isAnySocketOpen = TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen;
|
||||
if (OWSSignalService.sharedInstance.hasCensoredPhoneNumber) {
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyDisabled) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_MANUALLY_DISABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has "
|
||||
@"been manually disabled.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(
|
||||
@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_AUTO_ENABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has been "
|
||||
@"auto-enabled based on local phone number.");
|
||||
}
|
||||
} else if (isAnySocketOpen) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_WEBSOCKET_CONNECTED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is connected to the "
|
||||
@"Signal service.");
|
||||
} else if (!self.reachability.isReachable) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_NO_CONNECTION",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is not connected to the "
|
||||
@"internet.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER",
|
||||
@"Table footer for the 'censorship circumvention' section when censorship circumvention can be manually "
|
||||
@"enabled.");
|
||||
}
|
||||
|
||||
// Do enable if :
|
||||
//
|
||||
// * ...Censorship circumvention is already manually enabled (to allow users to disable it).
|
||||
//
|
||||
// Otherwise, don't enable if:
|
||||
//
|
||||
// * ...Censorship circumvention is already enabled based on the local phone number.
|
||||
// * ...The websocket is connected, since that demonstrates that no censorship is in effect.
|
||||
// * ...The internet is not reachable, since we don't want to let users to activate
|
||||
// censorship circumvention unnecessarily, e.g. if they just don't have a valid
|
||||
// internet connection.
|
||||
OWSTableSwitchBlock isCensorshipCircumventionOnBlock = ^{
|
||||
return OWSSignalService.sharedInstance.isCensorshipCircumventionActive;
|
||||
};
|
||||
Reachability *reachability = self.reachability;
|
||||
OWSTableSwitchBlock isManualCensorshipCircumventionOnEnabledBlock = ^{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (service.isCensorshipCircumventionActive) {
|
||||
return YES;
|
||||
} else if (service.hasCensoredPhoneNumber && service.isCensorshipCircumventionManuallyDisabled) {
|
||||
return YES;
|
||||
} else if (TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen) {
|
||||
return NO;
|
||||
} else {
|
||||
return reachability.isReachable;
|
||||
}
|
||||
};
|
||||
|
||||
[censorshipSection
|
||||
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION",
|
||||
@"Label for the 'manual censorship circumvention' switch.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"censorship_circumvention")
|
||||
isOnBlock:isCensorshipCircumventionOnBlock
|
||||
isEnabledBlock:isManualCensorshipCircumventionOnEnabledBlock
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableCensorshipCircumventionSwitch:)]];
|
||||
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyActivated) {
|
||||
OWSCountryMetadata *manualCensorshipCircumventionCountry =
|
||||
[weakSelf ensureManualCensorshipCircumventionCountry];
|
||||
OWSAssertDebug(manualCensorshipCircumventionCountry);
|
||||
NSString *text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_COUNTRY_FORMAT",
|
||||
@"Label for the 'manual censorship circumvention' country. Embeds {{the manual "
|
||||
@"censorship circumvention country}}."),
|
||||
manualCensorshipCircumventionCountry.localizedCountryName];
|
||||
[censorshipSection addItem:[OWSTableItem disclosureItemWithText:text
|
||||
actionBlock:^{
|
||||
[weakSelf showDomainFrontingCountryView];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:censorshipSection];
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)showDomainFrontingCountryView
|
||||
{
|
||||
DomainFrontingCountryViewController *vc = [DomainFrontingCountryViewController new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (OWSCountryMetadata *)ensureManualCensorshipCircumventionCountry
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSCountryMetadata *countryMetadata = nil;
|
||||
NSString *countryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = [PhoneNumber defaultCountryCode];
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = @"US";
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
OWSAssertDebug(countryMetadata);
|
||||
}
|
||||
|
||||
if (countryMetadata) {
|
||||
// Ensure the "manual censorship circumvention" country state is in sync.
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryCode;
|
||||
}
|
||||
|
||||
return countryMetadata;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)syncPushTokens
|
||||
{
|
||||
OWSSyncPushTokensJob *job =
|
||||
[[OWSSyncPushTokensJob alloc] initWithAccountManager:AppEnvironment.shared.accountManager
|
||||
preferences:Environment.shared.preferences];
|
||||
job.uploadOnlyIfStale = NO;
|
||||
[job run]
|
||||
.then(^{
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"PUSH_REGISTER_SUCCESS",
|
||||
@"Title of alert shown when push tokens sync job succeeds.")];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"REGISTRATION_BODY",
|
||||
@"Title of alert shown when push tokens sync job fails.")];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didToggleEnableLogSwitch:(UISwitch *)sender
|
||||
{
|
||||
if (!sender.isOn) {
|
||||
OWSLogInfo(@"disabling logging.");
|
||||
[[DebugLogger sharedLogger] wipeLogs];
|
||||
[[DebugLogger sharedLogger] disableFileLogging];
|
||||
} else {
|
||||
[[DebugLogger sharedLogger] enableFileLogging];
|
||||
OWSLogInfo(@"enabling logging.");
|
||||
}
|
||||
|
||||
[OWSPreferences setIsLoggingEnabled:sender.isOn];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)didToggleEnableCensorshipCircumventionSwitch:(UISwitch *)sender
|
||||
{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (sender.isOn) {
|
||||
service.isCensorshipCircumventionManuallyDisabled = NO;
|
||||
service.isCensorshipCircumventionManuallyActivated = YES;
|
||||
} else {
|
||||
service.isCensorshipCircumventionManuallyDisabled = YES;
|
||||
service.isCensorshipCircumventionManuallyActivated = NO;
|
||||
}
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -8,31 +8,24 @@
|
|||
#import "OWSBackup.h"
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import "SignalsNavigationController.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/iOSVersions.h>
|
||||
#import <SignalUtilitiesKit/AppSetup.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSNavigationController.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/VersionMigrations.h>
|
||||
#import <SignalUtilitiesKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWS2FAManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBatchMessageProcessor.h>
|
||||
#import <SignalUtilitiesKit/OWSDisappearingMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSIncompleteCallsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSMath.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SignalUtilitiesKit/OWSReadReceiptManager.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
|
@ -40,7 +33,7 @@
|
|||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
|
||||
#import <YapDatabase/YapDatabaseCryptoUtils.h>
|
||||
#import <sys/utsname.h>
|
||||
|
||||
|
@ -118,20 +111,6 @@ static NSTimeInterval launchStartedAt;
|
|||
return SSKEnvironment.shared.disappearingMessagesJob;
|
||||
}
|
||||
|
||||
- (TSSocketManager *)socketManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.socketManager);
|
||||
|
||||
return SSKEnvironment.shared.socketManager;
|
||||
}
|
||||
|
||||
- (OWSMessageManager *)messageManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.messageManager);
|
||||
|
||||
return SSKEnvironment.shared.messageManager;
|
||||
}
|
||||
|
||||
- (OWSWindowManager *)windowManager
|
||||
{
|
||||
return Environment.shared.windowManager;
|
||||
|
@ -279,18 +258,12 @@ static NSTimeInterval launchStartedAt;
|
|||
selector:@selector(registrationStateDidChange)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationLockDidChange:)
|
||||
name:NSNotificationName_2FAStateDidChange
|
||||
object:nil];
|
||||
|
||||
// Loki - Observe data nuke request notifications
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleDataNukeRequested:) name:NSNotification.dataNukeRequested object:nil];
|
||||
|
||||
OWSLogInfo(@"application: didFinishLaunchingWithOptions completed.");
|
||||
|
||||
[OWSAnalytics appLaunchDidBegin];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -394,41 +367,6 @@ static NSTimeInterval launchStartedAt;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)showLaunchFailureUI:(NSError *)error
|
||||
{
|
||||
// Disable normal functioning of app.
|
||||
self.didAppLaunchFail = YES;
|
||||
|
||||
// We perform a subset of the [application:didFinishLaunchingWithOptions:].
|
||||
[AppVersion sharedInstance];
|
||||
[self startupLogging];
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
|
||||
// Show the launch screen
|
||||
self.window.rootViewController =
|
||||
[[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController];
|
||||
|
||||
[self.window makeKeyAndVisible];
|
||||
|
||||
UIAlertController *alert =
|
||||
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_TITLE",
|
||||
@"Title for the 'app launch failed' alert.")
|
||||
message:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_MESSAGE",
|
||||
@"Message for the 'app launch failed' alert.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", nil)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[Pastelog submitLogsWithCompletion:^{
|
||||
OWSFail(@"Exiting after sharing debug logs.");
|
||||
}];
|
||||
}]];
|
||||
UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
|
||||
[fromViewController presentAlert:alert];
|
||||
}
|
||||
|
||||
- (void)startupLogging
|
||||
{
|
||||
OWSLogInfo(@"iOS Version: %@", [UIDevice currentDevice].systemVersion);
|
||||
|
@ -498,16 +436,6 @@ static NSTimeInterval launchStartedAt;
|
|||
[[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:self.primaryStorage] run];
|
||||
[[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:self.primaryStorage] run];
|
||||
});
|
||||
} else {
|
||||
OWSLogInfo(@"Running post launch block for unregistered user.");
|
||||
|
||||
// Unregistered user should have no unread messages. e.g. if you delete your account.
|
||||
[AppEnvironment.shared.notificationPresenter clearAllNotifications];
|
||||
|
||||
UITapGestureRecognizer *gesture =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:[Pastelog class] action:@selector(submitLogs)];
|
||||
gesture.numberOfTapsRequired = 8;
|
||||
[self.window addGestureRecognizer:gesture];
|
||||
}
|
||||
}); // end dispatchOnce for first time we become active
|
||||
|
||||
|
@ -640,8 +568,6 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[self ensureRootViewController];
|
||||
|
||||
[self.messageManager startObserving];
|
||||
|
||||
[self.udManager setup];
|
||||
|
||||
[self preheatDatabaseViews];
|
||||
|
@ -657,8 +583,6 @@ static NSTimeInterval launchStartedAt;
|
|||
if (appVersion.lastAppVersion.length > 0
|
||||
&& ![appVersion.lastAppVersion isEqualToString:appVersion.currentAppVersion]) {
|
||||
[[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
|
||||
|
||||
[SSKEnvironment.shared.syncManager sendConfigurationSyncMessage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -734,18 +658,6 @@ static NSTimeInterval launchStartedAt;
|
|||
[UIViewController attemptRotationToDeviceOrientation];
|
||||
}
|
||||
|
||||
#pragma mark - Status Bar Interaction
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
[super touchesBegan:touches withEvent:event];
|
||||
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
|
||||
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
|
||||
if (CGRectContainsPoint(statusBarFrame, location)) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:TappedStatusBarNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
||||
|
@ -953,7 +865,6 @@ static NSTimeInterval launchStartedAt;
|
|||
[[LKPushNotificationManager unregisterWithToken:deviceToken isForcedUpdate:YES] retainUntilComplete];
|
||||
}
|
||||
[ThreadUtil deleteAllContent];
|
||||
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
|
||||
[SSKEnvironment.shared.identityManager clearIdentityKey];
|
||||
[SNSnodeAPI clearSnodePool];
|
||||
[self stopPoller];
|
||||
|
|
|
@ -34,9 +34,6 @@ import SignalUtilitiesKit
|
|||
// @objc
|
||||
// public var outboundCallInitiator: OutboundCallInitiator
|
||||
|
||||
@objc
|
||||
public var messageFetcherJob: MessageFetcherJob
|
||||
|
||||
@objc
|
||||
public var accountManager: AccountManager
|
||||
|
||||
|
@ -81,9 +78,6 @@ import SignalUtilitiesKit
|
|||
|
||||
private override init() {
|
||||
self.callMessageHandler = WebRTCCallMessageHandler()
|
||||
// self.callService = CallService()
|
||||
// self.outboundCallInitiator = OutboundCallInitiator()
|
||||
self.messageFetcherJob = MessageFetcherJob()
|
||||
self.accountManager = AccountManager()
|
||||
self.notificationPresenter = NotificationPresenter()
|
||||
self.pushRegistrationManager = PushRegistrationManager()
|
||||
|
|
|
@ -120,14 +120,6 @@ protocol NotificationPresenterAdaptee: class {
|
|||
|
||||
func cancelNotifications(threadId: String)
|
||||
func clearAllNotifications()
|
||||
|
||||
var hasReceivedSyncMessageRecently: Bool { get }
|
||||
}
|
||||
|
||||
extension NotificationPresenterAdaptee {
|
||||
var hasReceivedSyncMessageRecently: Bool {
|
||||
return OWSDeviceManager.shared().hasReceivedSyncMessage(inLastSeconds: 60)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(OWSNotificationPresenter)
|
||||
|
@ -153,10 +145,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
var identityManager: OWSIdentityManager {
|
||||
return OWSIdentityManager.shared()
|
||||
}
|
||||
|
@ -209,140 +197,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return adaptee.registerNotificationSettings()
|
||||
}
|
||||
|
||||
// func presentIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
//
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.incomingCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.localCallId: call.localId.uuidString
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// self.adaptee.notify(category: .incomingCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: .defaultiOSIncomingRingtone,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func presentMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCallFromNoLongerVerifiedIdentity,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
|
||||
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||
|
||||
guard !thread.isMuted else {
|
||||
|
@ -359,7 +213,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// for more details.
|
||||
let messageText = DisplayableText.filterNotificationText(rawMessageText)
|
||||
|
||||
let senderName = OWSUserProfile.fetch(uniqueId: incomingMessage.authorId, transaction: transaction)?.profileName ?? contactsManager.displayName(forPhoneIdentifier: incomingMessage.authorId)
|
||||
let senderName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: incomingMessage.authorId, avoidingWriteTransaction: true)!
|
||||
|
||||
let notificationTitle: String?
|
||||
switch previewType {
|
||||
|
@ -401,12 +255,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// Don't reply from lockscreen if anyone in this conversation is
|
||||
// "no longer verified".
|
||||
var category = AppNotificationCategory.incomingMessage
|
||||
for recipientId in thread.recipientIdentifiers {
|
||||
if self.identityManager.verificationState(forRecipientId: recipientId) == .noLongerVerified {
|
||||
category = AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: threadId
|
||||
|
@ -554,14 +402,6 @@ class NotificationActionHandler {
|
|||
return SignalApp.shared()
|
||||
}
|
||||
|
||||
var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// var callUIAdapter: CallUIAdapter {
|
||||
// return AppEnvironment.shared.callService.callUIAdapter
|
||||
// }
|
||||
|
||||
var notificationPresenter: NotificationPresenter {
|
||||
return AppEnvironment.shared.notificationPresenter
|
||||
}
|
||||
|
@ -572,41 +412,6 @@ class NotificationActionHandler {
|
|||
|
||||
// MARK: -
|
||||
|
||||
// func answerCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.answerCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func callBack(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let recipientId = userInfo[AppNotificationUserInfoKey.callBackNumber] as? String else {
|
||||
// throw NotificationError.failDebug("recipientId was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: false)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func declineCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.declineCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
|
||||
func markAsRead(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
|
||||
throw NotificationError.failDebug("threadId was unexpectedly nil")
|
||||
|
@ -629,15 +434,18 @@ class NotificationActionHandler {
|
|||
}
|
||||
|
||||
return markAsRead(thread: thread).then { () -> Promise<Void> in
|
||||
let sendPromise = ThreadUtil.sendMessageNonDurably(text: replyText,
|
||||
thread: thread,
|
||||
quotedReplyModel: nil,
|
||||
messageSender: self.messageSender)
|
||||
// TODO TODO TODO
|
||||
|
||||
// let sendPromise = ThreadUtil.sendMessageNonDurably(text: replyText,
|
||||
// thread: thread,
|
||||
// quotedReplyModel: nil,
|
||||
// messageSender: self.messageSender)
|
||||
|
||||
return sendPromise.recover { error in
|
||||
Logger.warn("Failed to send reply message from notification with error: \(error)")
|
||||
self.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
}
|
||||
// return sendPromise.recover { error in
|
||||
// Logger.warn("Failed to send reply message from notification with error: \(error)")
|
||||
// self.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
// }
|
||||
return Promise<Void>.value(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,25 +474,6 @@ class NotificationActionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
extension ThreadUtil {
|
||||
static var dbReadConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
class func sendMessageNonDurably(text: String, thread: TSThread, quotedReplyModel: OWSQuotedReplyModel?, messageSender: MessageSender) -> Promise<Void> {
|
||||
return Promise { resolver in
|
||||
self.dbReadConnection.read { transaction in
|
||||
_ = self.sendMessageNonDurably(withText: text,
|
||||
in: thread,
|
||||
quotedReplyModel: quotedReplyModel,
|
||||
transaction: transaction,
|
||||
messageSender: messageSender,
|
||||
completion: resolver.resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
private let columns: UIStackView
|
||||
private let textRows: UIStackView
|
||||
private let avatarView: AvatarImageView
|
||||
// private let avatarView: AvatarImageView
|
||||
|
||||
private let _textLabel: UILabel
|
||||
override public var textLabel: UILabel? {
|
||||
|
@ -27,8 +27,8 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.avatarView = AvatarImageView()
|
||||
avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
// self.avatarView = AvatarImageView()
|
||||
// avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
|
||||
self._textLabel = UILabel()
|
||||
self._detailTextLabel = UILabel()
|
||||
|
@ -36,7 +36,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
self.textRows = UIStackView(arrangedSubviews: [_textLabel, _detailTextLabel])
|
||||
textRows.axis = .vertical
|
||||
|
||||
self.columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
self.columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.spacing = CGFloat(kContactCellAvatarTextMargin)
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public func configure(image: UIImage?, text: String?, detailText: String?) {
|
||||
self.avatarView.image = image
|
||||
// self.avatarView.image = image
|
||||
self.textLabel?.text = text
|
||||
self.detailTextLabel?.text = detailText
|
||||
|
||||
|
@ -65,7 +65,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
self.avatarView.image = nil
|
||||
// self.avatarView.image = nil
|
||||
self.textLabel?.text = nil
|
||||
self.detailTextLabel?.text = nil
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
#import "OWSNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BlockListViewController.h"
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactTableViewCell.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "PhoneNumber.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController () <ContactsViewHelperDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation BlockListViewController
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
|
||||
self.title
|
||||
= NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE", @"Label for the block list section of the settings view");
|
||||
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self addChildViewController:self.tableViewController];
|
||||
[_tableViewController.view autoPinEdgesToSuperviewEdges];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak BlockListViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
|
||||
// "Add" section
|
||||
|
||||
OWSTableSection *addSection = [OWSTableSection new];
|
||||
addSection.footerTitle = NSLocalizedString(
|
||||
@"BLOCK_USER_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of blocking another user.");
|
||||
|
||||
[addSection
|
||||
addItem:[OWSTableItem
|
||||
disclosureItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_ADD_BUTTON",
|
||||
@"A label for the 'add phone number' button in the block list table.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"add")
|
||||
actionBlock:^{
|
||||
AddToBlockListViewController *vc = [[AddToBlockListViewController alloc] init];
|
||||
[weakSelf.navigationController pushViewController:vc animated:YES];
|
||||
}]];
|
||||
[contents addSection:addSection];
|
||||
|
||||
// "Blocklist" section
|
||||
|
||||
NSArray<NSString *> *blockedPhoneNumbers =
|
||||
[self.blockingManager.blockedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
if (blockedPhoneNumbers.count > 0) {
|
||||
OWSTableSection *blockedContactsSection = [OWSTableSection new];
|
||||
blockedContactsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_USERS_SECTION", @"Section header for users that have been blocked");
|
||||
|
||||
for (NSString *phoneNumber in blockedPhoneNumbers) {
|
||||
[blockedContactsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
[cell configureWithRecipientId:phoneNumber];
|
||||
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
BlockListViewController, @"user");
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils
|
||||
showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedContactsSection];
|
||||
}
|
||||
|
||||
NSArray<TSGroupModel *> *blockedGroups = self.blockingManager.blockedGroups;
|
||||
if (blockedGroups.count > 0) {
|
||||
OWSTableSection *blockedGroupsSection = [OWSTableSection new];
|
||||
blockedGroupsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_GROUPS_SECTION", @"Section header for groups that have been blocked");
|
||||
|
||||
for (TSGroupModel *blockedGroup in blockedGroups) {
|
||||
UIImage *_Nullable image = blockedGroup.groupImage;
|
||||
if (!image) {
|
||||
NSString *conversationColorName =
|
||||
[TSGroupThread defaultConversationColorNameForGroupId:blockedGroup.groupId];
|
||||
image = [OWSGroupAvatarBuilder defaultAvatarForGroupId:blockedGroup.groupId
|
||||
conversationColorName:conversationColorName
|
||||
diameter:kStandardAvatarSize];
|
||||
}
|
||||
NSString *groupName
|
||||
= blockedGroup.groupName.length > 0 ? blockedGroup.groupName : TSGroupThread.defaultGroupName;
|
||||
|
||||
[blockedGroupsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
OWSAvatarTableViewCell *cell = [OWSAvatarTableViewCell new];
|
||||
[cell configureWithImage:image
|
||||
text:groupName
|
||||
detailText:nil];
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils showUnblockGroupActionSheet:blockedGroup
|
||||
displayName:groupName
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedGroupsSection];
|
||||
}
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,532 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class OWSColorPickerAccessoryView: NeverClearView {
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: kSwatchSize, height: kSwatchSize)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return self.intrinsicContentSize
|
||||
}
|
||||
|
||||
let kSwatchSize: CGFloat = 24
|
||||
|
||||
@objc
|
||||
required init(color: UIColor) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let circleView = CircleView()
|
||||
circleView.backgroundColor = color
|
||||
addSubview(circleView)
|
||||
circleView.autoSetDimensions(to: CGSize(width: kSwatchSize, height: kSwatchSize))
|
||||
circleView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@objc (OWSCircleView)
|
||||
class CircleView: UIView {
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
self.layer.cornerRadius = self.bounds.size.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorViewDelegate: class {
|
||||
func colorViewWasTapped(_ colorView: ColorView)
|
||||
}
|
||||
|
||||
class ColorView: UIView {
|
||||
public weak var delegate: ColorViewDelegate?
|
||||
public let conversationColor: OWSConversationColor
|
||||
|
||||
private let swatchView: CircleView
|
||||
private let selectedRing: CircleView
|
||||
public var isSelected: Bool = false {
|
||||
didSet {
|
||||
self.selectedRing.isHidden = !isSelected
|
||||
}
|
||||
}
|
||||
|
||||
required init(conversationColor: OWSConversationColor) {
|
||||
self.conversationColor = conversationColor
|
||||
self.swatchView = CircleView()
|
||||
self.selectedRing = CircleView()
|
||||
|
||||
super.init(frame: .zero)
|
||||
self.addSubview(selectedRing)
|
||||
self.addSubview(swatchView)
|
||||
|
||||
// Selected Ring
|
||||
let cellHeight: CGFloat = ScaleFromIPhone5(60)
|
||||
selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight))
|
||||
|
||||
selectedRing.layer.borderColor = Theme.secondaryColor.cgColor
|
||||
selectedRing.layer.borderWidth = 2
|
||||
selectedRing.autoPinEdgesToSuperviewEdges()
|
||||
selectedRing.isHidden = true
|
||||
|
||||
// Color Swatch
|
||||
swatchView.backgroundColor = conversationColor.primaryColor
|
||||
|
||||
let swatchSize: CGFloat = ScaleFromIPhone5(46)
|
||||
swatchView.autoSetDimensions(to: CGSize(width: swatchSize, height: swatchSize))
|
||||
|
||||
swatchView.autoCenterInSuperview()
|
||||
|
||||
// gestures
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func didTap() {
|
||||
delegate?.colorViewWasTapped(self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol ColorPickerDelegate: class {
|
||||
func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
@objc(OWSColorPicker)
|
||||
class ColorPicker: NSObject, ColorPickerViewDelegate {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ColorPickerDelegate?
|
||||
|
||||
@objc
|
||||
let sheetViewController: SheetViewController
|
||||
|
||||
@objc
|
||||
init(thread: TSThread) {
|
||||
let colorName = thread.conversationColorName
|
||||
let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName)
|
||||
sheetViewController = SheetViewController()
|
||||
|
||||
super.init()
|
||||
|
||||
let colorPickerView = ColorPickerView(thread: thread)
|
||||
colorPickerView.delegate = self
|
||||
colorPickerView.select(conversationColor: currentConversationColor)
|
||||
|
||||
sheetViewController.contentView.addSubview(colorPickerView)
|
||||
colorPickerView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
// MARK: ColorPickerViewDelegate
|
||||
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor) {
|
||||
self.delegate?.colorPicker(self, didPickConversationColor: conversationColor)
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorPickerViewDelegate: class {
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
class ColorPickerView: UIView, ColorViewDelegate {
|
||||
|
||||
private let colorViews: [ColorView]
|
||||
let conversationStyle: ConversationStyle
|
||||
var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
weak var delegate: ColorPickerViewDelegate?
|
||||
|
||||
// This is mostly a developer convenience - OWSMessageCell asserts at some point
|
||||
// that the available method width is greater than 0.
|
||||
// We ultimately use the width of the picker view which will be larger.
|
||||
let kMinimumConversationWidth: CGFloat = 300
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
updateMockConversationView()
|
||||
}
|
||||
}
|
||||
|
||||
let mockConversationView: UIView = UIView()
|
||||
|
||||
init(thread: TSThread) {
|
||||
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
|
||||
|
||||
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
|
||||
|
||||
self.conversationStyle = ConversationStyle(thread: thread)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
colorViews.forEach { $0.delegate = self }
|
||||
|
||||
let headerView = self.buildHeaderView()
|
||||
mockConversationView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
mockConversationView.backgroundColor = Theme.backgroundColor
|
||||
self.updateMockConversationView()
|
||||
|
||||
let paletteView = self.buildPaletteView(colorViews: colorViews)
|
||||
|
||||
let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView])
|
||||
rowsStackView.axis = .vertical
|
||||
addSubview(rowsStackView)
|
||||
rowsStackView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: ColorViewDelegate
|
||||
|
||||
func colorViewWasTapped(_ colorView: ColorView) {
|
||||
self.select(conversationColor: colorView.conversationColor)
|
||||
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
|
||||
updateMockConversationView()
|
||||
}
|
||||
|
||||
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
|
||||
colorViews.forEach { colorView in
|
||||
colorView.isSelected = colorView.conversationColor == selectedConversationColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View Building
|
||||
|
||||
private func buildHeaderView() -> UIView {
|
||||
let headerView = UIView()
|
||||
headerView.layoutMargins = UIEdgeInsets(top: 15, left: 16, bottom: 15, right: 16)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("COLOR_PICKER_SHEET_TITLE", comment: "Modal Sheet title when picking a conversation color.")
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
|
||||
headerView.addSubview(titleLabel)
|
||||
titleLabel.ows_autoPinToSuperviewMargins()
|
||||
|
||||
let bottomBorderView = UIView()
|
||||
bottomBorderView.backgroundColor = Theme.hairlineColor
|
||||
headerView.addSubview(bottomBorderView)
|
||||
bottomBorderView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||
bottomBorderView.autoSetDimension(.height, toSize: CGHairlineWidth())
|
||||
|
||||
return headerView
|
||||
}
|
||||
|
||||
private func updateMockConversationView() {
|
||||
/*
|
||||
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
|
||||
mockConversationView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
// outgoing
|
||||
outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let outgoingItem = MockConversationViewItem()
|
||||
let outgoingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_1", comment: "The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble.")
|
||||
outgoingItem.interaction = MockOutgoingMessage(messageBody: outgoingText)
|
||||
outgoingItem.displayableBodyText = DisplayableText.displayableText(outgoingText)
|
||||
outgoingItem.interactionType = .outgoingMessage
|
||||
|
||||
outgoingMessageView.viewItem = outgoingItem
|
||||
outgoingMessageView.cellMediaCache = NSCache()
|
||||
outgoingMessageView.conversationStyle = conversationStyle
|
||||
outgoingMessageView.configureViews()
|
||||
outgoingMessageView.loadContent()
|
||||
let outgoingCell = UIView()
|
||||
outgoingCell.addSubview(outgoingMessageView)
|
||||
outgoingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
|
||||
let outgoingSize = outgoingMessageView.measureSize()
|
||||
outgoingMessageView.autoSetDimensions(to: outgoingSize)
|
||||
|
||||
// incoming
|
||||
incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let incomingItem = MockConversationViewItem()
|
||||
let incomingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_2", comment: "The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble.")
|
||||
incomingItem.interaction = MockIncomingMessage(messageBody: incomingText)
|
||||
incomingItem.displayableBodyText = DisplayableText.displayableText(incomingText)
|
||||
incomingItem.interactionType = .incomingMessage
|
||||
|
||||
incomingMessageView.viewItem = incomingItem
|
||||
incomingMessageView.cellMediaCache = NSCache()
|
||||
incomingMessageView.conversationStyle = conversationStyle
|
||||
incomingMessageView.configureViews()
|
||||
incomingMessageView.loadContent()
|
||||
let incomingCell = UIView()
|
||||
incomingCell.addSubview(incomingMessageView)
|
||||
incomingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
|
||||
let incomingSize = incomingMessageView.measureSize()
|
||||
incomingMessageView.autoSetDimensions(to: incomingSize)
|
||||
|
||||
let messagesStackView = UIStackView(arrangedSubviews: [outgoingCell, incomingCell])
|
||||
messagesStackView.axis = .vertical
|
||||
messagesStackView.spacing = 12
|
||||
|
||||
mockConversationView.addSubview(messagesStackView)
|
||||
messagesStackView.autoPinEdgesToSuperviewMargins()
|
||||
*/
|
||||
}
|
||||
|
||||
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
|
||||
let paletteView = UIView()
|
||||
let paletteMargin = ScaleFromIPhone5(12)
|
||||
paletteView.layoutMargins = UIEdgeInsets(top: paletteMargin, left: paletteMargin, bottom: 0, right: paletteMargin)
|
||||
|
||||
let kRowLength = 4
|
||||
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
|
||||
let row = UIStackView(arrangedSubviews: colorViewsInRow)
|
||||
row.distribution = UIStackView.Distribution.equalSpacing
|
||||
return row
|
||||
}
|
||||
let rowsStackView = UIStackView(arrangedSubviews: rows)
|
||||
rowsStackView.axis = .vertical
|
||||
rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 30)
|
||||
|
||||
paletteView.addSubview(rowsStackView)
|
||||
rowsStackView.ows_autoPinToSuperviewMargins()
|
||||
|
||||
// no-op gesture to keep taps from dismissing SheetView
|
||||
paletteView.addGestureRecognizer(UITapGestureRecognizer(target: nil, action: nil))
|
||||
return paletteView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mock Classes for rendering demo conversation
|
||||
|
||||
/*
|
||||
@objc
|
||||
private class MockConversationViewItem: NSObject, ConversationViewItem {
|
||||
var userCanDeleteGroupMessage: Bool = false
|
||||
var isRSSFeed: Bool = false
|
||||
var interaction: TSInteraction = TSMessage()
|
||||
var interactionType: OWSInteractionType = OWSInteractionType.unknown
|
||||
var quotedReply: OWSQuotedReplyModel?
|
||||
var isGroupThread: Bool = false
|
||||
var hasBodyText: Bool = true
|
||||
var isQuotedReply: Bool = false
|
||||
var hasQuotedAttachment: Bool = false
|
||||
var hasQuotedText: Bool = false
|
||||
var hasCellHeader: Bool = false
|
||||
var isExpiringMessage: Bool = false
|
||||
var shouldShowDate: Bool = false
|
||||
var shouldShowSenderAvatar: Bool = false
|
||||
var senderName: NSAttributedString?
|
||||
var shouldHideFooter: Bool = false
|
||||
var isFirstInCluster: Bool = true
|
||||
var isLastInCluster: Bool = true
|
||||
var unreadIndicator: OWSUnreadIndicator?
|
||||
var lastAudioMessageView: OWSAudioMessageView?
|
||||
var audioDurationSeconds: CGFloat = 0
|
||||
var audioProgressSeconds: CGFloat = 0
|
||||
var messageCellType: OWSMessageCellType = .textOnlyMessage
|
||||
var displayableBodyText: DisplayableText?
|
||||
var attachmentStream: TSAttachmentStream?
|
||||
var attachmentPointer: TSAttachmentPointer?
|
||||
var mediaSize: CGSize = .zero
|
||||
var displayableQuotedText: DisplayableText?
|
||||
var quotedAttachmentMimetype: String?
|
||||
var quotedRecipientId: String?
|
||||
var didCellMediaFailToLoad: Bool = false
|
||||
var contactShare: ContactShareViewModel?
|
||||
var systemMessageText: String?
|
||||
var authorConversationColorName: String?
|
||||
var hasBodyTextActionContent: Bool = false
|
||||
var hasMediaActionContent: Bool = false
|
||||
var mediaAlbumItems: [ConversationMediaAlbumItem]?
|
||||
var hasCachedLayoutState: Bool = false
|
||||
var linkPreview: OWSLinkPreview?
|
||||
var linkPreviewAttachment: TSAttachment?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func itemId() -> String {
|
||||
return interaction.uniqueId!
|
||||
}
|
||||
|
||||
func dequeueCell(for collectionView: UICollectionView, indexPath: IndexPath) -> ConversationViewCell {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return ConversationViewCell(forAutoLayout: ())
|
||||
}
|
||||
|
||||
func replace(_ interaction: TSInteraction, transaction: YapDatabaseReadTransaction) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func clearCachedLayoutState() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func saveMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func deleteAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func canCopyMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func canSaveMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func audioPlaybackState() -> AudioPlaybackState {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return AudioPlaybackState.paused
|
||||
}
|
||||
|
||||
func setAudioPlaybackState(_ state: AudioPlaybackState) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func cellSize() -> CGSize {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return CGSize.zero
|
||||
}
|
||||
|
||||
func vSpacing(withPreviousLayoutItem previousLayoutItem: ConversationViewLayoutItem) -> CGFloat {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return 2
|
||||
}
|
||||
|
||||
func firstValidAlbumAttachment() -> TSAttachmentStream? {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaAlbumHasFailedAttachment() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private class MockIncomingMessage: TSIncomingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(incomingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: TSThread(),
|
||||
authorId: "+fake-id",
|
||||
sourceDeviceId: 1,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil,
|
||||
serverTimestamp: nil,
|
||||
wasReceivedByUD: false)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
}
|
||||
|
||||
private class MockOutgoingMessage: TSOutgoingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: nil,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
expireStartedAt: 0,
|
||||
isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
|
||||
class MockOutgoingMessageRecipientState: TSOutgoingMessageRecipientState {
|
||||
override var state: OWSOutgoingMessageRecipientState {
|
||||
return OWSOutgoingMessageRecipientState.sent
|
||||
}
|
||||
|
||||
override var deliveryTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
|
||||
override var readTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
}
|
||||
|
||||
override func readRecipientIds() -> [String] {
|
||||
// makes message appear as read
|
||||
return ["fake-non-empty-id"]
|
||||
}
|
||||
|
||||
override func recipientState(forRecipientId recipientId: String) -> TSOutgoingMessageRecipientState? {
|
||||
return MockOutgoingMessageRecipientState()
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class ContactCell: UITableViewCell {
|
||||
|
||||
public static let kSeparatorHInset: CGFloat = CGFloat(kAvatarDiameter) + 16 + 8
|
||||
|
||||
static let kAvatarSpacing: CGFloat = 6
|
||||
static let kAvatarDiameter: UInt = 40
|
||||
|
||||
let contactImageView: AvatarImageView
|
||||
let textStackView: UIStackView
|
||||
let titleLabel: UILabel
|
||||
var subtitleLabel: UILabel
|
||||
|
||||
var contact: Contact?
|
||||
var showsWhenSelected: Bool = false
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.contactImageView = AvatarImageView()
|
||||
self.textStackView = UIStackView()
|
||||
self.titleLabel = UILabel()
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel = UILabel()
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectionStyle = UITableViewCell.SelectionStyle.none
|
||||
|
||||
textStackView.axis = .vertical
|
||||
textStackView.addArrangedSubview(titleLabel)
|
||||
|
||||
contactImageView.autoSetDimensions(to: CGSize(width: CGFloat(ContactCell.kAvatarDiameter), height: CGFloat(ContactCell.kAvatarDiameter)))
|
||||
|
||||
let contentColumns: UIStackView = UIStackView(arrangedSubviews: [contactImageView, textStackView])
|
||||
contentColumns.axis = .horizontal
|
||||
contentColumns.spacing = ContactCell.kAvatarSpacing
|
||||
contentColumns.alignment = .center
|
||||
|
||||
self.contentView.addSubview(contentColumns)
|
||||
contentColumns.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
accessoryType = .none
|
||||
self.subtitleLabel.removeFromSuperview()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if showsWhenSelected {
|
||||
accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didChangePreferredContentSize() {
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
}
|
||||
|
||||
func configure(contact: Contact, subtitleType: SubtitleCellValue, showsWhenSelected: Bool, contactsManager: OWSContactsManager) {
|
||||
|
||||
OWSTableItem.configureCell(self)
|
||||
|
||||
self.contact = contact
|
||||
self.showsWhenSelected = showsWhenSelected
|
||||
|
||||
self.titleLabel.textColor = Theme.primaryColor
|
||||
self.subtitleLabel.textColor = Theme.secondaryColor
|
||||
|
||||
let cnContact = contactsManager.cnContact(withId: contact.cnContactId)
|
||||
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
|
||||
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
||||
|
||||
if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
|
||||
contactImageView.image = contactImage
|
||||
} else {
|
||||
let contactIdForDeterminingBackgroundColor: String
|
||||
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
|
||||
contactIdForDeterminingBackgroundColor = signalId
|
||||
} else {
|
||||
contactIdForDeterminingBackgroundColor = contact.fullName
|
||||
}
|
||||
|
||||
let avatarBuilder = OWSContactAvatarBuilder(nonSignalName: contact.fullName,
|
||||
colorSeed: contactIdForDeterminingBackgroundColor,
|
||||
diameter: ContactCell.kAvatarDiameter)
|
||||
|
||||
contactImageView.image = avatarBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSubtitle(subtitleType: SubtitleCellValue, contact: Contact) {
|
||||
switch subtitleType {
|
||||
case .none:
|
||||
assert(self.subtitleLabel.superview == nil)
|
||||
break
|
||||
case .phoneNumber:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstPhoneNumber = contact.userTextPhoneNumbers.first {
|
||||
self.subtitleLabel.text = firstPhoneNumber
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
|
||||
}
|
||||
case .email:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstEmail = contact.emails.first {
|
||||
self.subtitleLabel.text = firstEmail
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Bold the sorting portion of the name. e.g. if we sort by family name, bold the family name.
|
||||
*/
|
||||
func formattedFullName(font: UIFont) -> NSAttributedString? {
|
||||
let keyToHighlight = ContactSortOrder == .familyName ? CNContactFamilyNameKey : CNContactGivenNameKey
|
||||
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)
|
||||
let boldAttributes = [
|
||||
NSAttributedString.Key.font: UIFont(descriptor: boldDescriptor!, size: 0)
|
||||
]
|
||||
|
||||
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
|
||||
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
|
||||
highlightedName.enumerateAttributes(in: NSRange(location: 0, length: highlightedName.length), options: [], using: { (attrs, range, _) in
|
||||
if let property = attrs[NSAttributedString.Key(rawValue: CNContactPropertyAttribute)] as? String, property == keyToHighlight {
|
||||
highlightedName.addAttributes(boldAttributes, range: range)
|
||||
}
|
||||
})
|
||||
return highlightedName
|
||||
}
|
||||
|
||||
if let emailAddress = self.emailAddresses.first?.value {
|
||||
return NSAttributedString(string: emailAddress as String, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
if let phoneNumber = self.phoneNumbers.first?.value.stringValue {
|
||||
return NSAttributedString(string: phoneNumber, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
@objc
|
||||
public protocol ContactShareViewHelperDelegate: class {
|
||||
func didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
|
||||
|
||||
@objc
|
||||
weak var delegate: ContactShareViewHelperDelegate?
|
||||
|
||||
let contactsManager: OWSContactsManager
|
||||
|
||||
@objc
|
||||
public required init(contactsManager: OWSContactsManager) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.contactsManager = contactsManager
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
public func sendMessage(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .compose, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func audioCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .audioCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func videoCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .videoCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
private func presentThreadAndPeform(action: ConversationViewAction, contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
// TODO: We're taking the first Signal account id. We might
|
||||
// want to let the user select if there's more than one.
|
||||
let phoneNumbers = contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("missing Signal recipient id.")
|
||||
return
|
||||
}
|
||||
guard phoneNumbers.count > 1 else {
|
||||
let recipientId = phoneNumbers.first!
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
showPhoneNumberPicker(phoneNumbers: phoneNumbers, fromViewController: fromViewController, completion: { (recipientId) in
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
@objc
|
||||
public func showInviteContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: ""))
|
||||
return
|
||||
}
|
||||
let phoneNumbers = contactShare.e164PhoneNumbers()
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("no phone numbers.")
|
||||
return
|
||||
}
|
||||
|
||||
let inviteFlow = InviteFlow(presentingViewController: fromViewController)
|
||||
inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
|
||||
@objc
|
||||
func showAddToContacts(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressCreateNewContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressAddToExistingContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
private func showPhoneNumberPicker(phoneNumbers: [String], fromViewController: UIViewController, completion :@escaping ((String) -> Void)) {
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
for phoneNumber in phoneNumbers {
|
||||
actionSheet.addAction(UIAlertAction(title: PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber),
|
||||
style: .default) { _ in
|
||||
completion(phoneNumber)
|
||||
})
|
||||
}
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func didPressCreateNewContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentNewContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
func didPressAddToExistingContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentSelectAddToExistingContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func presentNewContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord, imageData: contactShare.avatarImageData) else {
|
||||
owsFailDebug("Could not derive system contact.")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
let contactViewController = CNContactViewController(forNewContact: systemContact)
|
||||
contactViewController.delegate = self
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(didFinishEditingContact))
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
fromViewController.present(modal, animated: true)
|
||||
}
|
||||
|
||||
private func presentSelectAddToExistingContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
guard let navigationController = fromViewController.navigationController else {
|
||||
owsFailDebug("missing navigationController")
|
||||
return
|
||||
}
|
||||
|
||||
let viewController = AddContactShareToExistingContactViewController(contactShare: contactShare)
|
||||
navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
@objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc public func didFinishEditingContact() {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
}
|
|
@ -1,679 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
import Reachability
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
class ContactViewController: OWSViewController, ContactShareViewHelperDelegate {
|
||||
|
||||
enum ContactViewMode {
|
||||
case systemContactWithSignal,
|
||||
systemContactWithoutSignal,
|
||||
nonSystemContact,
|
||||
noPhoneNumber,
|
||||
unknown
|
||||
}
|
||||
|
||||
private var hasLoadedView = false
|
||||
|
||||
private var viewMode = ContactViewMode.unknown {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if oldValue != viewMode && hasLoadedView {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contactsManager: OWSContactsManager
|
||||
|
||||
private var reachability: Reachability?
|
||||
|
||||
private let contactShare: ContactShareViewModel
|
||||
|
||||
private var contactShareViewHelper: ContactShareViewHelper
|
||||
|
||||
private weak var postDismissNavigationController: UINavigationController?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@available(*, unavailable, message: "use init(call:) constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
contactsManager = Environment.shared.contactsManager
|
||||
self.contactShare = contactShare
|
||||
self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
updateMode()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
|
||||
reachability = Reachability.forInternetConnection()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// self.navigationController is nil in viewWillDisappear when transition via message/call buttons
|
||||
// so we maintain our own reference to restore the navigation bars.
|
||||
postDismissNavigationController = navigationController
|
||||
navigationController.isNavigationBarHidden = true
|
||||
|
||||
contactsManager.requestSystemContactsOnce(completion: { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
})
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if self.presentedViewController == nil {
|
||||
// No need to do this when we're disappearing due to a modal presentation.
|
||||
// We'll eventually return to to this view and need to hide again. But also, there is a visible
|
||||
// animation glitch where the navigation bar for this view controller starts to appear while
|
||||
// the whole nav stack is about to be obscured by the modal we are presenting.
|
||||
guard let postDismissNavigationController = self.postDismissNavigationController else {
|
||||
owsFailDebug("postDismissNavigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
postDismissNavigationController.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
self.view.preservesSuperviewLayoutMargins = false
|
||||
self.view.backgroundColor = heroBackgroundColor()
|
||||
|
||||
updateContent()
|
||||
|
||||
hasLoadedView = true
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard contactShare.e164PhoneNumbers().count > 0 else {
|
||||
viewMode = .noPhoneNumber
|
||||
return
|
||||
}
|
||||
if systemContactsWithSignalAccountsForContact().count > 0 {
|
||||
viewMode = .systemContactWithSignal
|
||||
return
|
||||
}
|
||||
if systemContactsForContact().count > 0 {
|
||||
viewMode = .systemContactWithoutSignal
|
||||
return
|
||||
}
|
||||
|
||||
viewMode = .nonSystemContact
|
||||
}
|
||||
|
||||
private func systemContactsWithSignalAccountsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func systemContactsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func updateContent() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let rootView = self.view else {
|
||||
owsFailDebug("missing root view.")
|
||||
return
|
||||
}
|
||||
|
||||
for subview in rootView.subviews {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
let topView = createTopView()
|
||||
rootView.addSubview(topView)
|
||||
topView.autoPinEdge(.top, to: .top, of: view)
|
||||
topView.autoPinWidthToSuperview()
|
||||
|
||||
// This view provides a background "below the fold".
|
||||
let bottomView = UIView.container()
|
||||
bottomView.backgroundColor = Theme.backgroundColor
|
||||
self.view.addSubview(bottomView)
|
||||
bottomView.layoutMargins = .zero
|
||||
bottomView.autoPinWidthToSuperview()
|
||||
bottomView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
bottomView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.preservesSuperviewLayoutMargins = false
|
||||
self.view.addSubview(scrollView)
|
||||
scrollView.layoutMargins = .zero
|
||||
scrollView.autoPinWidthToSuperview()
|
||||
scrollView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let fieldsView = createFieldsView()
|
||||
|
||||
scrollView.addSubview(fieldsView)
|
||||
fieldsView.autoPinLeadingToSuperviewMargin()
|
||||
fieldsView.autoPinTrailingToSuperviewMargin()
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .top)
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
}
|
||||
|
||||
private func heroBackgroundColor() -> UIColor {
|
||||
return (Theme.isDarkThemeEnabled
|
||||
? UIColor(rgbHex: 0x272727)
|
||||
: UIColor(rgbHex: 0xefeff4))
|
||||
}
|
||||
|
||||
private func createTopView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let topView = UIView.container()
|
||||
topView.backgroundColor = heroBackgroundColor()
|
||||
topView.preservesSuperviewLayoutMargins = false
|
||||
|
||||
// Back Button
|
||||
let backButtonSize = CGFloat(50)
|
||||
let backButton = TappableView(actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressDismiss()
|
||||
})
|
||||
backButton.autoSetDimension(.width, toSize: backButtonSize)
|
||||
backButton.autoSetDimension(.height, toSize: backButtonSize)
|
||||
topView.addSubview(backButton)
|
||||
backButton.autoPinEdge(toSuperviewEdge: .top)
|
||||
backButton.autoPinLeadingToSuperviewMargin()
|
||||
|
||||
let backIconName = (CurrentAppContext().isRTL ? "system_disclosure_indicator" : "system_disclosure_indicator_rtl")
|
||||
guard let backIconImage = UIImage(named: backIconName) else {
|
||||
owsFailDebug("missing icon.")
|
||||
return topView
|
||||
}
|
||||
let backIconView = UIImageView(image: backIconImage.withRenderingMode(.alwaysTemplate))
|
||||
backIconView.contentMode = .scaleAspectFit
|
||||
backIconView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
backButton.addSubview(backIconView)
|
||||
backIconView.autoCenterInSuperview()
|
||||
|
||||
let avatarSize: CGFloat = 100
|
||||
let avatarView = AvatarImageView()
|
||||
avatarView.image = contactShare.getAvatarImage(diameter: avatarSize, contactsManager: contactsManager)
|
||||
topView.addSubview(avatarView)
|
||||
avatarView.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
|
||||
avatarView.autoHCenterInSuperview()
|
||||
avatarView.autoSetDimension(.width, toSize: avatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: avatarSize)
|
||||
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = contactShare.displayName
|
||||
nameLabel.font = UIFont.ows_dynamicTypeTitle1
|
||||
nameLabel.textColor = Theme.primaryColor
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
nameLabel.textAlignment = .center
|
||||
topView.addSubview(nameLabel)
|
||||
nameLabel.autoPinEdge(.top, to: .bottom, of: avatarView, withOffset: 10)
|
||||
nameLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
nameLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
var lastView: UIView = nameLabel
|
||||
|
||||
for phoneNumber in systemContactsWithSignalAccountsForContact() {
|
||||
let phoneNumberLabel = UILabel()
|
||||
phoneNumberLabel.text = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber)
|
||||
phoneNumberLabel.font = UIFont.ows_dynamicTypeFootnote
|
||||
phoneNumberLabel.textColor = Theme.primaryColor
|
||||
phoneNumberLabel.lineBreakMode = .byTruncatingTail
|
||||
phoneNumberLabel.textAlignment = .center
|
||||
topView.addSubview(phoneNumberLabel)
|
||||
phoneNumberLabel.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 5)
|
||||
phoneNumberLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
phoneNumberLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = phoneNumberLabel
|
||||
}
|
||||
|
||||
switch viewMode {
|
||||
case .systemContactWithSignal:
|
||||
// Show actions buttons for system contacts with a Signal account.
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
imageName: "contact_view_message",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressSendMessage()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
imageName: "contact_view_audio_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAudioCall()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
imageName: "contact_view_video_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressVideoCall()
|
||||
}))
|
||||
topView.addSubview(stackView)
|
||||
stackView.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
stackView.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
stackView.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = stackView
|
||||
case .systemContactWithoutSignal:
|
||||
// Show invite button for system contacts without a Signal account.
|
||||
let inviteButton = createLargePillButton(text: NSLocalizedString("ACTION_INVITE",
|
||||
comment: "Label for 'invite' button in contact view."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressInvite()
|
||||
})
|
||||
topView.addSubview(inviteButton)
|
||||
inviteButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
inviteButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
inviteButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = inviteButton
|
||||
case .nonSystemContact:
|
||||
// Show no action buttons for non-system contacts.
|
||||
break
|
||||
case .noPhoneNumber:
|
||||
// Show no action buttons for contacts without a phone number.
|
||||
break
|
||||
case .unknown:
|
||||
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
|
||||
topView.addSubview(activityIndicator)
|
||||
activityIndicator.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 10)
|
||||
activityIndicator.autoHCenterInSuperview()
|
||||
lastView = activityIndicator
|
||||
break
|
||||
}
|
||||
|
||||
// Always show "add to contacts" button.
|
||||
let addToContactsButton = createLargePillButton(text: NSLocalizedString("CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
comment: "Message shown in conversation view that offers to add an unknown user to your phone's contacts."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddToContacts()
|
||||
})
|
||||
topView.addSubview(addToContactsButton)
|
||||
addToContactsButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
addToContactsButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
addToContactsButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = addToContactsButton
|
||||
|
||||
lastView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 15)
|
||||
|
||||
return topView
|
||||
}
|
||||
|
||||
private func createFieldsView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
var rows = [UIView]()
|
||||
|
||||
// TODO: Not designed yet.
|
||||
// if viewMode == .systemContactWithSignal ||
|
||||
// viewMode == .systemContactWithoutSignal {
|
||||
// addRow(createActionRow(labelText:NSLocalizedString("ACTION_SHARE_CONTACT",
|
||||
// comment:"Label for 'share contact' button."),
|
||||
// action:#selector(didPressShareContact)))
|
||||
// }
|
||||
|
||||
if let organizationName = contactShare.name.organizationName?.ows_stripped() {
|
||||
if (contactShare.name.hasAnyNamePart() &&
|
||||
organizationName.count > 0) {
|
||||
rows.append(ContactFieldView.contactFieldView(forOrganizationName: organizationName,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin)))
|
||||
}
|
||||
}
|
||||
|
||||
for phoneNumber in contactShare.phoneNumbers {
|
||||
rows.append(ContactFieldView.contactFieldView(forPhoneNumber: phoneNumber,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressPhoneNumber(phoneNumber: phoneNumber)
|
||||
}))
|
||||
}
|
||||
|
||||
for email in contactShare.emails {
|
||||
rows.append(ContactFieldView.contactFieldView(forEmail: email,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressEmail(email: email)
|
||||
}))
|
||||
}
|
||||
|
||||
for address in contactShare.addresses {
|
||||
rows.append(ContactFieldView.contactFieldView(forAddress: address,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddress(address: address)
|
||||
}))
|
||||
}
|
||||
|
||||
return ContactFieldView(rows: rows, hMargin: hMargin)
|
||||
}
|
||||
|
||||
private let hMargin = CGFloat(16)
|
||||
|
||||
private func createActionRow(labelText: String, action: Selector) -> UIView {
|
||||
let row = UIView()
|
||||
row.layoutMargins.left = 0
|
||||
row.layoutMargins.right = 0
|
||||
row.isUserInteractionEnabled = true
|
||||
row.addGestureRecognizer(UITapGestureRecognizer(target: self, action: action))
|
||||
|
||||
let label = UILabel()
|
||||
label.text = labelText
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
row.addSubview(label)
|
||||
label.autoPinTopToSuperviewMargin()
|
||||
label.autoPinBottomToSuperviewMargin()
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// TODO: Use real assets.
|
||||
private func createCircleActionButton(text: String, imageName: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let buttonSize = CGFloat(50)
|
||||
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.width, toSize: buttonSize, relation: .greaterThanOrEqual)
|
||||
|
||||
let circleView = UIView()
|
||||
circleView.backgroundColor = Theme.backgroundColor
|
||||
circleView.autoSetDimension(.width, toSize: buttonSize)
|
||||
circleView.autoSetDimension(.height, toSize: buttonSize)
|
||||
circleView.layer.cornerRadius = buttonSize * 0.5
|
||||
button.addSubview(circleView)
|
||||
circleView.autoPinEdge(toSuperviewEdge: .top)
|
||||
circleView.autoHCenterInSuperview()
|
||||
|
||||
guard let image = UIImage(named: imageName) else {
|
||||
owsFailDebug("missing image.")
|
||||
return button
|
||||
}
|
||||
let imageView = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
circleView.addSubview(imageView)
|
||||
imageView.autoCenterInSuperview()
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeCaption2
|
||||
label.textColor = Theme.primaryColor
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinEdge(.top, to: .bottom, of: circleView, withOffset: 3)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
label.autoPinLeadingToSuperviewMargin()
|
||||
label.autoPinTrailingToSuperviewMargin()
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private func createLargePillButton(text: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.backgroundColor = Theme.backgroundColor
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.height, toSize: 45)
|
||||
button.layer.cornerRadius = 5
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: 20)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: 20)
|
||||
label.autoVCenterInSuperview()
|
||||
label.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
func didPressShareContact(sender: UIGestureRecognizer) {
|
||||
Logger.info("")
|
||||
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func didPressSendMessage() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.sendMessage(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAudioCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.audioCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressVideoCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.videoCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressInvite() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showInviteContact(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAddToContacts() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showAddToContacts(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressDismiss() {
|
||||
Logger.info("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func didPressPhoneNumber(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
if let e164 = phoneNumber.tryToConvertToE164() {
|
||||
if contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager).contains(e164) {
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .compose, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .audioCall, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .videoCall, animated: true)
|
||||
})
|
||||
} else {
|
||||
// TODO: We could offer callPhoneNumberWithSystemCall.
|
||||
}
|
||||
}
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = phoneNumber.phoneNumber
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func callPhoneNumberWithSystemCall(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "tel:\(phoneNumber.phoneNumber)") else {
|
||||
owsFailDebug("could not open phone number.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressEmail(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_EMAIL_IN_EMAIL_APP",
|
||||
comment: "Label for 'open email in email app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openEmailInEmailApp(email: email)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = email.email
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openEmailInEmailApp(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "mailto:\(email.email)") else {
|
||||
owsFailDebug("could not open email.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressAddress(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_ADDRESS_IN_MAPS_APP",
|
||||
comment: "Label for 'open address in maps app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openAddressInMaps(address: address)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
UIPasteboard.general.string = strongSelf.formatAddressForQuery(address: address)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openAddressInMaps(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let mapAddress = formatAddressForQuery(address: address)
|
||||
guard let escapedMapAddress = mapAddress.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
// Note that we use "q" (i.e. query) rather than "address" since we can't assume
|
||||
// this is a well-formed address.
|
||||
guard let url = URL(string: "http://maps.apple.com/?q=\(escapedMapAddress)") else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func formatAddressForQuery(address: OWSContactAddress) -> String {
|
||||
Logger.info("")
|
||||
|
||||
// Open address in Apple Maps app.
|
||||
var addressParts = [String]()
|
||||
let addAddressPart: ((String?) -> Void) = { (part) in
|
||||
guard let part = part else {
|
||||
return
|
||||
}
|
||||
guard part.count > 0 else {
|
||||
return
|
||||
}
|
||||
addressParts.append(part)
|
||||
}
|
||||
addAddressPart(address.street)
|
||||
addAddressPart(address.neighborhood)
|
||||
addAddressPart(address.city)
|
||||
addAddressPart(address.region)
|
||||
addAddressPart(address.postcode)
|
||||
addAddressPart(address.country)
|
||||
return addressParts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
// MARK: - ContactShareViewHelperDelegate
|
||||
|
||||
public func didCreateOrEditContact() {
|
||||
Logger.info("")
|
||||
updateContent()
|
||||
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
// Originally based on EPContacts
|
||||
//
|
||||
// Created by Prabaharan Elangovan on 12/10/15.
|
||||
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc
|
||||
public protocol ContactsPickerDelegate: class {
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError)
|
||||
func contactsPickerDidCancel(_: ContactsPicker)
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum SubtitleCellValue: Int {
|
||||
case phoneNumber, email, none
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
|
||||
|
||||
var tableView: UITableView!
|
||||
var searchBar: UISearchBar!
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let contactCellReuseIdentifier = "contactCellReuseIdentifier"
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
|
||||
// If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
|
||||
// the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
|
||||
override public var canBecomeFirstResponder: Bool {
|
||||
Logger.debug("")
|
||||
return true
|
||||
}
|
||||
|
||||
override public func becomeFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override public func resignFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.resignFirstResponder()
|
||||
}
|
||||
|
||||
private let collation = UILocalizedIndexedCollation.current()
|
||||
public var collationForTests: UILocalizedIndexedCollation {
|
||||
get {
|
||||
return collation
|
||||
}
|
||||
}
|
||||
private let contactStore = CNContactStore()
|
||||
|
||||
// Data Source State
|
||||
private lazy var sections = [[CNContact]]()
|
||||
private lazy var filteredSections = [[CNContact]]()
|
||||
private lazy var selectedContacts = [Contact]()
|
||||
|
||||
// Configuration
|
||||
@objc
|
||||
public weak var contactsPickerDelegate: ContactsPickerDelegate?
|
||||
private let subtitleCellType: SubtitleCellValue
|
||||
private let allowsMultipleSelection: Bool
|
||||
private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@objc
|
||||
required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
self.allowsMultipleSelection = allowsMultipleSelection
|
||||
self.subtitleCellType = subtitleCellType
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle Methods
|
||||
|
||||
override public func loadView() {
|
||||
self.view = UIView()
|
||||
let tableView = UITableView()
|
||||
self.tableView = tableView
|
||||
self.tableView.separatorColor = Theme.cellSeparatorColor
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .top)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .leading)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .trailing)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
let searchBar = OWSSearchBar()
|
||||
self.searchBar = searchBar
|
||||
searchBar.delegate = self
|
||||
searchBar.sizeToFit()
|
||||
|
||||
tableView.tableHeaderView = searchBar
|
||||
}
|
||||
|
||||
override open func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor
|
||||
self.tableView.backgroundColor = Theme.backgroundColor
|
||||
|
||||
searchBar.placeholder = NSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")
|
||||
|
||||
// Auto size cells for dynamic type
|
||||
tableView.estimatedRowHeight = 60.0
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 60
|
||||
|
||||
tableView.allowsMultipleSelection = allowsMultipleSelection
|
||||
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
|
||||
|
||||
registerContactCell()
|
||||
initializeBarButtons()
|
||||
reloadContacts()
|
||||
updateSearchResults(searchText: "")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didChangePreferredContentSize() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
private func initializeBarButtons() {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
|
||||
self.navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
if allowsMultipleSelection {
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
|
||||
self.navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
}
|
||||
|
||||
private func registerContactCell() {
|
||||
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
|
||||
}
|
||||
|
||||
// MARK: - Contact Operations
|
||||
|
||||
private func reloadContacts() {
|
||||
getContacts( onError: { error in
|
||||
Logger.error("failed to reload contacts with error:\(error)")
|
||||
})
|
||||
}
|
||||
|
||||
private func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
|
||||
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
|
||||
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
|
||||
let title = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE", comment: "Alert title when contacts disabled while trying to invite contacts to signal")
|
||||
let body = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY", comment: "Alert body when contacts disabled while trying to invite contacts to signal")
|
||||
|
||||
let alert = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
|
||||
let dismissText = CommonStrings.cancelButton
|
||||
|
||||
let cancelAction = UIAlertAction(title: dismissText, style: .cancel, handler: { _ in
|
||||
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
||||
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
|
||||
errorHandler(error)
|
||||
})
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
let settingsText = CommonStrings.openSettingsButton
|
||||
let openSettingsAction = UIAlertAction(title: settingsText, style: .default, handler: { (_) in
|
||||
UIApplication.shared.openSystemSettings()
|
||||
})
|
||||
alert.addAction(openSettingsAction)
|
||||
|
||||
self.presentAlert(alert)
|
||||
|
||||
case CNAuthorizationStatus.notDetermined:
|
||||
//This case means the user is prompted for the first time for allowing contacts
|
||||
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
|
||||
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
|
||||
if granted {
|
||||
self.getContacts(onError: errorHandler)
|
||||
} else {
|
||||
errorHandler(error!)
|
||||
}
|
||||
}
|
||||
|
||||
case CNAuthorizationStatus.authorized:
|
||||
//Authorization granted by user for this app.
|
||||
var contacts = [CNContact]()
|
||||
|
||||
do {
|
||||
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
|
||||
contactFetchRequest.sortOrder = .userDefault
|
||||
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
|
||||
contacts.append(contact)
|
||||
}
|
||||
self.sections = collatedContacts(contacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("Failed to fetch contacts with error:\(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
|
||||
let selector: Selector = #selector(getter: CNContact.nameForCollating)
|
||||
|
||||
var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
|
||||
for contact in contacts {
|
||||
let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
|
||||
collated[sectionNumber].append(contact)
|
||||
}
|
||||
return collated
|
||||
}
|
||||
|
||||
// MARK: - Table View DataSource
|
||||
|
||||
open func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.collation.sectionTitles.count
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return dataSource[section].count
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegates
|
||||
|
||||
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
|
||||
owsFailDebug("cell had unexpected type")
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
let dataSource = filteredSections
|
||||
let cnContact = dataSource[indexPath.section][indexPath.row]
|
||||
let contact = Contact(systemContact: cnContact)
|
||||
|
||||
cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
|
||||
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
||||
cell.isSelected = isSelected
|
||||
|
||||
// Make sure we preserve selection across tableView.reloadData which happens when toggling between
|
||||
// search controller
|
||||
if (isSelected) {
|
||||
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let deselectedContact = cell.contact!
|
||||
|
||||
selectedContacts = selectedContacts.filter {
|
||||
return $0.uniqueId != deselectedContact.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
Logger.verbose("")
|
||||
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let selectedContact = cell.contact!
|
||||
|
||||
guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
selectedContacts.append(selectedContact)
|
||||
|
||||
if !allowsMultipleSelection {
|
||||
// Single selection code
|
||||
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||
return collation.section(forSectionIndexTitle: index)
|
||||
}
|
||||
|
||||
open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
|
||||
return collation.sectionIndexTitles
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't show empty sections
|
||||
if dataSource[section].count > 0 {
|
||||
guard section < collation.sectionTitles.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return collation.sectionTitles[section]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button Actions
|
||||
|
||||
@objc func onTouchCancelButton() {
|
||||
contactsPickerDelegate?.contactsPickerDidCancel(self)
|
||||
}
|
||||
|
||||
@objc func onTouchDoneButton() {
|
||||
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
||||
}
|
||||
|
||||
// MARK: - Search Actions
|
||||
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
updateSearchResults(searchText: searchText)
|
||||
}
|
||||
|
||||
open func updateSearchResults(searchText: String) {
|
||||
let predicate: NSPredicate
|
||||
if searchText.isEmpty {
|
||||
filteredSections = sections
|
||||
} else {
|
||||
do {
|
||||
predicate = CNContact.predicateForContacts(matchingName: searchText)
|
||||
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
|
||||
filteredSections = collatedContacts(filteredContacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("updating search results failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
let ContactSortOrder = computeSortOrder()
|
||||
|
||||
func computeSortOrder() -> CNContactSortOrder {
|
||||
let comparator = CNContact.comparator(forNameSortOrder: .userDefault)
|
||||
|
||||
let contact0 = CNMutableContact()
|
||||
contact0.givenName = "A"
|
||||
contact0.familyName = "Z"
|
||||
|
||||
let contact1 = CNMutableContact()
|
||||
contact1.givenName = "Z"
|
||||
contact1.familyName = "A"
|
||||
|
||||
let result = comparator(contact0, contact1)
|
||||
|
||||
if result == .orderedAscending {
|
||||
return .givenName
|
||||
} else {
|
||||
return .familyName
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Sorting Key used by collation
|
||||
*/
|
||||
@objc var nameForCollating: String {
|
||||
get {
|
||||
if self.familyName.isEmpty && self.givenName.isEmpty {
|
||||
return self.emailAddresses.first?.value as String? ?? ""
|
||||
}
|
||||
|
||||
let compositeName: String
|
||||
if ContactSortOrder == .familyName {
|
||||
compositeName = "\(self.familyName) \(self.givenName)"
|
||||
} else {
|
||||
compositeName = "\(self.givenName) \(self.familyName)"
|
||||
}
|
||||
return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class ConversationConfigurationSyncOperation: OWSOperation {
|
||||
|
||||
enum ColorSyncOperationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
||||
private var dbConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
private var messageSenderJobQueue: MessageSenderJobQueue {
|
||||
return SSKEnvironment.shared.messageSenderJobQueue
|
||||
}
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
private var syncManager: OWSSyncManagerProtocol {
|
||||
return SSKEnvironment.shared.syncManager
|
||||
}
|
||||
|
||||
private let thread: TSThread
|
||||
|
||||
@objc
|
||||
public init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func run() {
|
||||
if let contactThread = thread as? TSContactThread {
|
||||
sync(contactThread: contactThread)
|
||||
} else if let groupThread = thread as? TSGroupThread {
|
||||
sync(groupThread: groupThread)
|
||||
} else {
|
||||
self.reportAssertionError(description: "unknown thread type")
|
||||
}
|
||||
}
|
||||
|
||||
private func reportAssertionError(description: String) {
|
||||
let error = ColorSyncOperationError.assertionError(description: description)
|
||||
self.reportError(error)
|
||||
}
|
||||
|
||||
private func sync(contactThread: TSContactThread) {
|
||||
guard let signalAccount: SignalAccount = self.contactsManager.fetchSignalAccount(forRecipientId: contactThread.contactIdentifier()) else {
|
||||
reportAssertionError(description: "unable to find signalAccount")
|
||||
return
|
||||
}
|
||||
|
||||
syncManager.syncContacts(for: [signalAccount]).retainUntilComplete()
|
||||
}
|
||||
|
||||
private func sync(groupThread: TSGroupThread) {
|
||||
// TODO sync only the affected group
|
||||
// The current implementation works, but seems wasteful.
|
||||
// Does desktop handle single group sync correctly?
|
||||
// What does Android do?
|
||||
let syncMessage: OWSSyncGroupsMessage = OWSSyncGroupsMessage(groupThread: groupThread)
|
||||
|
||||
var dataSource: DataSource?
|
||||
self.dbConnection.read { transaction in
|
||||
guard let messageData: Data = syncMessage.buildPlainTextAttachmentData(with: transaction) else {
|
||||
owsFailDebug("could not serialize sync groups data")
|
||||
return
|
||||
}
|
||||
dataSource = DataSourceValue.dataSource(withSyncMessageData: messageData)
|
||||
}
|
||||
|
||||
guard let attachmentDataSource = dataSource else {
|
||||
self.reportAssertionError(description: "unable to build attachment data source")
|
||||
return
|
||||
}
|
||||
|
||||
self.sendConfiguration(attachmentDataSource: attachmentDataSource, syncMessage: syncMessage)
|
||||
}
|
||||
|
||||
private func sendConfiguration(attachmentDataSource: DataSource, syncMessage: OWSOutgoingSyncMessage) {
|
||||
self.messageSenderJobQueue.add(mediaMessage: syncMessage,
|
||||
dataSource: attachmentDataSource,
|
||||
contentType: OWSMimeTypeApplicationOctetStream,
|
||||
sourceFilename: nil,
|
||||
caption: nil,
|
||||
albumMessageId: nil,
|
||||
isTemporaryAttachment: true)
|
||||
self.reportSuccess()
|
||||
}
|
||||
|
||||
}
|
|
@ -35,23 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - System Cell
|
||||
|
||||
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId;
|
||||
- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage;
|
||||
- (void)tappedCorruptedMessage:(TSErrorMessage *)message;
|
||||
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message;
|
||||
- (void)showFingerprintWithRecipientId:(NSString *)recipientId;
|
||||
- (void)showConversationSettings;
|
||||
- (void)handleCallTap:(TSCall *)call;
|
||||
|
||||
#pragma mark - Offers
|
||||
|
||||
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
#pragma mark - Formatting
|
||||
|
||||
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId;
|
||||
|
||||
#pragma mark - Caching
|
||||
|
||||
|
@ -61,10 +47,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
|
||||
|
||||
#pragma mark - Contacts
|
||||
|
||||
- (OWSContactsManager *)contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactOffersInteraction;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactOffersCell : ConversationViewCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,263 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactOffersCell ()
|
||||
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) UIButton *addToContactsButton;
|
||||
@property (nonatomic) UIButton *addToProfileWhitelistButton;
|
||||
@property (nonatomic) UIButton *blockButton;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
|
||||
@property (nonatomic) UIStackView *stackView;
|
||||
@property (nonatomic) UIStackView *buttonStackView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactOffersCell
|
||||
|
||||
// `[UIView init]` invokes `[self initWithFrame:...]`.
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self commontInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commontInit
|
||||
{
|
||||
OWSAssertDebug(!self.titleLabel);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.contentView.layoutMargins = UIEdgeInsetsZero;
|
||||
self.layoutConstraints = @[];
|
||||
|
||||
self.titleLabel = [UILabel new];
|
||||
self.titleLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_CONTACTS_OFFER_TITLE",
|
||||
@"Title for the group of buttons show for unknown contacts offering to add them to contacts, etc.");
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
self.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
self.addToContactsButton = [self
|
||||
createButtonWithTitle:
|
||||
NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.")
|
||||
selector:@selector(addToContacts)];
|
||||
self.addToProfileWhitelistButton = [self
|
||||
createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_ADD_USER_TO_PROFILE_WHITELIST_OFFER",
|
||||
@"Message shown in conversation view that offers to share your profile with a user.")
|
||||
selector:@selector(addToProfileWhitelist)];
|
||||
self.blockButton =
|
||||
[self createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_UNKNOWN_CONTACT_BLOCK_OFFER",
|
||||
@"Message shown in conversation view that offers to block an unknown user.")
|
||||
selector:@selector(block)];
|
||||
|
||||
UIStackView *buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:self.buttons];
|
||||
buttonStackView.axis = UILayoutConstraintAxisVertical;
|
||||
buttonStackView.spacing = self.vSpacing;
|
||||
self.buttonStackView = buttonStackView;
|
||||
|
||||
self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.titleLabel,
|
||||
buttonStackView,
|
||||
]];
|
||||
self.stackView.axis = UILayoutConstraintAxisVertical;
|
||||
self.stackView.spacing = self.vSpacing;
|
||||
self.stackView.alignment = UIStackViewAlignmentCenter;
|
||||
[self.contentView addSubview:self.stackView];
|
||||
}
|
||||
|
||||
- (void)configureFonts
|
||||
{
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeSubheadlineFont;
|
||||
|
||||
UIFont *buttonFont = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
|
||||
self.addToContactsButton.titleLabel.font = buttonFont;
|
||||
self.addToProfileWhitelistButton.titleLabel.font = buttonFont;
|
||||
self.blockButton.titleLabel.font = buttonFont;
|
||||
}
|
||||
|
||||
- (UIButton *)createButtonWithTitle:(NSString *)title selector:(SEL)selector
|
||||
{
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[button setTitle:title forState:UIControlStateNormal];
|
||||
button.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
button.layer.cornerRadius = 4.f;
|
||||
[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
|
||||
button.contentEdgeInsets = UIEdgeInsetsMake(0, 10.f, 0, 10.f);
|
||||
return button;
|
||||
}
|
||||
|
||||
+ (NSString *)cellReuseIdentifier
|
||||
{
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
self.backgroundColor = [Theme backgroundColor];
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
self.titleLabel.textColor = Theme.secondaryColor;
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button setTitleColor:[UIColor ows_signalBlueColor] forState:UIControlStateNormal];
|
||||
[button setBackgroundColor:Theme.conversationButtonBackgroundColor];
|
||||
}
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
OWSAssertDebug(
|
||||
interaction.hasBlockOffer || interaction.hasAddToContactsOffer || interaction.hasAddToProfileWhitelistOffer);
|
||||
|
||||
self.addToContactsButton.hidden = !interaction.hasAddToContactsOffer;
|
||||
self.addToProfileWhitelistButton.hidden = !interaction.hasAddToProfileWhitelistOffer;
|
||||
self.blockButton.hidden = !interaction.hasBlockOffer;
|
||||
|
||||
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
|
||||
self.layoutConstraints = @[
|
||||
[self.addToContactsButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.addToProfileWhitelistButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.blockButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading
|
||||
withInset:self.conversationStyle.fullWidthGutterLeading],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
|
||||
withInset:self.conversationStyle.fullWidthGutterTrailing],
|
||||
];
|
||||
|
||||
// This hack fixes a bug that I don't understand.
|
||||
//
|
||||
// On an iPhone 5C running iOS 10.3.3,
|
||||
//
|
||||
// * Alice is a contact for which we should show some but not all contact offer buttons.
|
||||
// * Delete thread with Alice.
|
||||
// * Send yourself a message from Alice.
|
||||
// * Open conversation with Alice.
|
||||
//
|
||||
// Expected: Some (but not all) offer buttons are displayed.
|
||||
// Observed: All offer buttons are displayed, in a cramped layout.
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button removeFromSuperview];
|
||||
}
|
||||
for (UIButton *button in self.buttons) {
|
||||
if (!button.hidden) {
|
||||
[self.buttonStackView addArrangedSubview:button];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<UIButton *> *)buttons
|
||||
{
|
||||
return @[
|
||||
self.addToContactsButton,
|
||||
self.addToProfileWhitelistButton,
|
||||
self.blockButton,
|
||||
];
|
||||
}
|
||||
|
||||
- (CGFloat)topVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)bottomVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)vSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
- (CGFloat)buttonHeight
|
||||
{
|
||||
return (24.f + self.addToContactsButton.titleLabel.font.lineHeight);
|
||||
}
|
||||
|
||||
- (CGSize)cellSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0);
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
||||
result.height += ceil([self.titleLabel sizeThatFits:CGSizeZero].height);
|
||||
|
||||
int buttonCount = ((interaction.hasBlockOffer ? 1 : 0) + (interaction.hasAddToContactsOffer ? 1 : 0)
|
||||
+ (interaction.hasAddToProfileWhitelistOffer ? 1 : 0));
|
||||
result.height += buttonCount * (self.vSpacing + self.buttonHeight);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (nullable OWSContactOffersInteraction *)interaction
|
||||
{
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug(self.viewItem.interaction);
|
||||
if (![self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) {
|
||||
OWSFailDebug(@"expected OWSContactOffersInteraction but found: %@", self.viewItem.interaction);
|
||||
return nil;
|
||||
}
|
||||
return (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
}
|
||||
|
||||
- (void)addToContacts
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToContactsOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)addToProfileWhitelist
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToProfileWhitelistOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)block
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedUnknownContactBlockOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
|
||||
@protocol OWSContactShareButtonsViewDelegate <NSObject>
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactShareButtonsView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
// Returns YES IFF the tap was handled.
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender;
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareButtonsView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
@property (nonatomic, weak) id<OWSContactShareButtonsViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@property (nonatomic, nullable) UIView *buttonView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareButtonsView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_delegate = delegate;
|
||||
_contactShare = contactShare;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
[self createContents];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return [contactShare e164PhoneNumbers].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
return [self hasAnyButton:contactShare contactsManager:contactsManager];
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return ([self hasSendTextButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasInviteButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasAddToContactsButton:contactShare]);
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.buttonHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonHeight
|
||||
{
|
||||
return MAX(44.f, self.buttonFont.lineHeight + self.buttonVMargin * 2);
|
||||
}
|
||||
|
||||
+ (UIFont *)buttonFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont].ows_mediumWeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonVMargin
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
OWSAssertDebug([OWSContactShareButtonsView hasAnyButton:self.contactShare contactsManager:self.contactsManager]);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.backgroundColor = Theme.conversationButtonBackgroundColor;
|
||||
|
||||
UILabel *label = [UILabel new];
|
||||
self.buttonView = label;
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text
|
||||
= NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for button that lets you send a message to a contact.");
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view.");
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.");
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button state.");
|
||||
}
|
||||
label.font = OWSContactShareButtonsView.buttonFont;
|
||||
label.textColor = (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_materialBlueColor);
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:label];
|
||||
[label ows_autoPinToSuperviewEdges];
|
||||
[label autoSetDimension:ALDimensionHeight toSize:OWSContactShareButtonsView.buttonHeight];
|
||||
|
||||
self.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *tap =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
|
||||
[self addGestureRecognizer:tap];
|
||||
}
|
||||
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender
|
||||
{
|
||||
if (!self.buttonView) {
|
||||
return NO;
|
||||
}
|
||||
CGPoint location = [sender locationInView:self.buttonView];
|
||||
if (!CGRectContainsPoint(self.buttonView.bounds, location)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendMessageToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendInviteToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:self.contactShare];
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button tap.");
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
@class ConversationStyle;
|
||||
|
||||
@interface OWSContactShareView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
- (void)createContents;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,165 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isIncoming;
|
||||
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_contactShare = contactShare;
|
||||
_isIncoming = isIncoming;
|
||||
_conversationStyle = conversationStyle;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (CGFloat)hMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)vMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconHSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)contentHeight
|
||||
{
|
||||
CGFloat labelsHeight = (self.nameFont.lineHeight + self.labelsVSpacing + self.subtitleFont.lineHeight);
|
||||
CGFloat contentHeight = MAX(self.iconSize, labelsHeight);
|
||||
contentHeight += OWSContactShareView.vMargin * 2;
|
||||
return contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)iconSize
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return [OWSContactShareView iconSize];
|
||||
}
|
||||
|
||||
+ (UIFont *)nameFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont];
|
||||
}
|
||||
|
||||
+ (UIFont *)subtitleFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeCaption1Font];
|
||||
}
|
||||
|
||||
+ (CGFloat)labelsVSpacing
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
UIColor *textColor = [self.conversationStyle bubbleTextColorWithIsIncoming:self.isIncoming];
|
||||
|
||||
AvatarImageView *avatarView = [AvatarImageView new];
|
||||
avatarView.image =
|
||||
[self.contactShare getAvatarImageWithDiameter:self.iconSize contactsManager:self.contactsManager];
|
||||
|
||||
[avatarView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
|
||||
[avatarView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
|
||||
[avatarView setCompressionResistanceHigh];
|
||||
[avatarView setContentHuggingHigh];
|
||||
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = self.contactShare.displayName;
|
||||
topLabel.textColor = textColor;
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
topLabel.font = OWSContactShareView.nameFont;
|
||||
|
||||
UIStackView *labelsView = [UIStackView new];
|
||||
labelsView.axis = UILayoutConstraintAxisVertical;
|
||||
labelsView.spacing = OWSContactShareView.labelsVSpacing;
|
||||
[labelsView addArrangedSubview:topLabel];
|
||||
|
||||
NSString *_Nullable firstPhoneNumber =
|
||||
[self.contactShare systemContactsWithSignalAccountPhoneNumbers:self.contactsManager].firstObject;
|
||||
if (firstPhoneNumber.length > 0) {
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
bottomLabel.text = [PhoneNumber bestEffortLocalizedPhoneNumberWithE164:firstPhoneNumber];
|
||||
bottomLabel.textColor = [self.conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
bottomLabel.font = OWSContactShareView.subtitleFont;
|
||||
[labelsView addArrangedSubview:bottomLabel];
|
||||
}
|
||||
|
||||
UIImage *disclosureImage =
|
||||
[UIImage imageNamed:(CurrentAppContext().isRTL ? @"small_chevron_left" : @"small_chevron_right")];
|
||||
OWSAssertDebug(disclosureImage);
|
||||
UIImageView *disclosureImageView = [UIImageView new];
|
||||
disclosureImageView.image = [disclosureImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
disclosureImageView.tintColor = textColor;
|
||||
[disclosureImageView setCompressionResistanceHigh];
|
||||
[disclosureImageView setContentHuggingHigh];
|
||||
|
||||
UIStackView *hStackView = [UIStackView new];
|
||||
hStackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
hStackView.spacing = self.iconHSpacing;
|
||||
hStackView.alignment = UIStackViewAlignmentCenter;
|
||||
hStackView.layoutMarginsRelativeArrangement = YES;
|
||||
hStackView.layoutMargins
|
||||
= UIEdgeInsetsMake(OWSContactShareView.vMargin, self.hMargin, OWSContactShareView.vMargin, self.hMargin);
|
||||
[hStackView addArrangedSubview:avatarView];
|
||||
[hStackView addArrangedSubview:labelsView];
|
||||
[hStackView addArrangedSubview:disclosureImageView];
|
||||
[self addSubview:hStackView];
|
||||
[hStackView ows_autoPinToSuperviewEdges];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -7,7 +7,7 @@
|
|||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/MimeTypeUtil.h>
|
||||
|
@ -91,7 +91,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (CGFloat)iconHeight
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
return 48.0f;
|
||||
}
|
||||
|
||||
- (void)createContentsWithConversationStyle:(ConversationStyle *)conversationStyle
|
||||
|
|
|
@ -49,15 +49,6 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
|
|||
|
||||
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
|
||||
|
||||
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendMessage(toContactShare:));
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendInvite(toContactShare:));
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
|
||||
|
||||
@end
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
#import "ConversationViewItem.h"
|
||||
#import "OWSBubbleShapeView.h"
|
||||
#import "OWSBubbleView.h"
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "OWSLabel.h"
|
||||
#import "OWSMessageFooterView.h"
|
||||
|
@ -20,7 +18,7 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate>
|
||||
|
||||
@property (nonatomic) OWSBubbleView *bubbleView;
|
||||
|
||||
|
@ -48,8 +46,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property (nonatomic) OWSMessageFooterView *footerView;
|
||||
|
||||
@property (nonatomic, nullable) OWSContactShareButtonsView *contactShareButtonsView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
@ -267,9 +263,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_GenericAttachment:
|
||||
bodyMediaView = [self loadViewForGenericAttachment];
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
bodyMediaView = [self loadViewForContactShare];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
bodyMediaView = [self loadViewForMediaAlbum];
|
||||
break;
|
||||
|
@ -300,24 +293,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (self.hasBodyMediaWithThumbnail) {
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
} else {
|
||||
OWSAssertDebug(self.cellType == OWSMessageCellType_ContactShare);
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[textViews addObject:bodyMediaView];
|
||||
|
@ -414,86 +389,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.CGSizeValue.height]];
|
||||
}
|
||||
|
||||
[self insertContactShareButtonsIfNecessary];
|
||||
|
||||
[self updateBubbleColor];
|
||||
|
||||
[self configureBubbleRounding];
|
||||
}
|
||||
|
||||
- (void)insertContactShareButtonsIfNecessary
|
||||
{
|
||||
if (self.cellType != OWSMessageCellType_ContactShare) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareButtonsView *buttonsView =
|
||||
[[OWSContactShareButtonsView alloc] initWithContactShare:self.viewItem.contactShare delegate:self];
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
OWSAssertDebug(actionButtonsSize);
|
||||
[self.viewConstraints addObjectsFromArray:@[
|
||||
[buttonsView autoSetDimension:ALDimensionHeight toSize:actionButtonsSize.CGSizeValue.height],
|
||||
]];
|
||||
|
||||
// The "contact share" view casts a shadow "downward" onto adjacent views,
|
||||
// so we use a "proxy" view to take its place within the v-stack
|
||||
// view and then insert the "contact share" view above its proxy so that
|
||||
// it floats above the other content of the bubble view.
|
||||
|
||||
UIView *proxyView = [UIView new];
|
||||
[self.stackView addArrangedSubview:proxyView];
|
||||
|
||||
OWSBubbleShapeView *shadowView = [[OWSBubbleShapeView alloc] initShadow];
|
||||
OWSBubbleShapeView *clipView = [[OWSBubbleShapeView alloc] initClip];
|
||||
|
||||
[self addSubview:shadowView];
|
||||
[self addSubview:clipView];
|
||||
|
||||
[self.viewConstraints addObjectsFromArray:[shadowView autoPinToEdgesOfView:proxyView]];
|
||||
[self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]];
|
||||
|
||||
[clipView addSubview:buttonsView];
|
||||
[self.viewConstraints addObjectsFromArray:[buttonsView ows_autoPinToSuperviewEdges]];
|
||||
|
||||
[self.bubbleView addPartnerView:shadowView];
|
||||
[self.bubbleView addPartnerView:clipView];
|
||||
|
||||
// Prevent the layer from animating changes.
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
|
||||
OWSAssertDebug(buttonsView.backgroundColor);
|
||||
shadowView.fillColor = buttonsView.backgroundColor;
|
||||
shadowView.layer.shadowColor = Theme.boldColor.CGColor;
|
||||
shadowView.layer.shadowOpacity = 0.12f;
|
||||
shadowView.layer.shadowOffset = CGSizeZero;
|
||||
shadowView.layer.shadowRadius = 1.f;
|
||||
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerTop
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && (self.isQuotedReply || !self.shouldShowSenderName));
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerBottom
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && !self.hasBottomFooter);
|
||||
}
|
||||
|
||||
- (CGFloat)contactShareVSpacing
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)senderNameBottomSpacing
|
||||
{
|
||||
return 0.f;
|
||||
|
@ -557,7 +457,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return NO;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
|
@ -572,7 +471,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return YES;
|
||||
|
@ -581,8 +479,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)hasFullWidthMediaView
|
||||
{
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_ContactShare
|
||||
|| self.cellType == OWSMessageCellType_MediaMessage);
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_MediaMessage);
|
||||
}
|
||||
|
||||
- (BOOL)canFooterOverlayMedia
|
||||
|
@ -876,26 +773,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return attachmentView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForContactShare
|
||||
{
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare
|
||||
isIncoming:self.isIncoming
|
||||
conversationStyle:self.conversationStyle];
|
||||
[contactShareView createContents];
|
||||
// TODO: Should we change appearance if contact avatar is uploading?
|
||||
|
||||
self.loadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
|
||||
return contactShareView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForOversizeTextDownload
|
||||
{
|
||||
// We can use an empty view. The progress views will display download
|
||||
|
@ -1079,11 +956,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
result = [attachmentView measureSizeWithMaxMessageWidth:maxMessageWidth];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]);
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
result = [OWSMediaAlbumCellView layoutSizeForMaxMessageWidth:maxMessageWidth
|
||||
items:self.viewItem.mediaAlbumItems];
|
||||
|
@ -1186,23 +1058,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [NSValue valueWithCGSize:result];
|
||||
}
|
||||
|
||||
- (nullable NSValue *)actionButtonsSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
|
||||
|
||||
if (self.cellType == OWSMessageCellType_ContactShare) {
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
if ([OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
CGSize buttonsSize = CGSizeCeil(
|
||||
CGSizeMake(self.conversationStyle.maxMessageWidth, [OWSContactShareButtonsView bubbleHeight]));
|
||||
return [NSValue valueWithCGSize:buttonsSize];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGSize)measureSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
|
@ -1239,13 +1094,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[textViewSizes addObject:bodyMediaSize];
|
||||
bodyMediaSize = nil;
|
||||
}
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMediaSize || quotedMessageSize) {
|
||||
|
@ -1296,12 +1144,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
cellSize.height += self.tapForMoreHeight + self.textViewVSpacing;
|
||||
}
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
if (actionButtonsSize) {
|
||||
cellSize.width = MAX(cellSize.width, actionButtonsSize.CGSizeValue.width);
|
||||
cellSize.height += actionButtonsSize.CGSizeValue.height;
|
||||
}
|
||||
|
||||
cellSize = CGSizeCeil(cellSize);
|
||||
|
||||
OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
|
||||
|
@ -1395,9 +1237,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
[self.contactShareButtonsView removeFromSuperview];
|
||||
self.contactShareButtonsView = nil;
|
||||
|
||||
[self.linkPreviewView removeFromSuperview];
|
||||
self.linkPreviewView.state = nil;
|
||||
}
|
||||
|
@ -1430,12 +1269,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
if (self.contactShareButtonsView) {
|
||||
if ([self.contactShareButtonsView handleTapGesture:sender]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CGPoint locationInMessageBubble = [sender locationInView:self];
|
||||
switch ([self gestureLocationForLocation:locationInMessageBubble]) {
|
||||
case OWSMessageGestureLocation_Default:
|
||||
|
@ -1488,9 +1321,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[AttachmentSharing showShareUIForAttachment:self.viewItem.attachmentStream];
|
||||
}
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
[self.delegate didTapContactShareViewItem:self.viewItem];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage: {
|
||||
OWSAssertDebug(self.bodyMediaView);
|
||||
OWSAssertDebug(self.viewItem.mediaAlbumItems.count > 0);
|
||||
|
@ -1603,32 +1433,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Sent quoted replies should not be cancellable.");
|
||||
}
|
||||
|
||||
#pragma mark - OWSContactShareButtonsViewDelegate
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendMessageToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendInviteToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:contactShare];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "OWSMessageBubbleView.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "Session-Swift.h"
|
||||
|
@ -286,7 +285,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.avatarView update];
|
||||
|
||||
// Loki: Show the moderator icon if needed
|
||||
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
if (self.viewItem.isGroupThread) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
__block SNOpenGroup *publicChat;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
|
||||
|
|
|
@ -122,7 +122,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSContactThread *thread = [outgoingMessage.thread as:TSContactThread.class];
|
||||
if (thread != nil) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction];
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
isNoteToSelf = ([thread.contactIdentifier isEqual:userPublicKey]);
|
||||
}
|
||||
}];
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#import "OWSBubbleView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
|
@ -550,8 +550,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 4;
|
|||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
}
|
||||
} else {
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: [contactsManager contactOrProfileNameForPhoneIdentifier:self.quotedMessage.authorId];
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId];
|
||||
|
||||
if (quotedAuthor == self.quotedMessage.authorId) {
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
|
|
|
@ -9,9 +9,8 @@
|
|||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSVerificationStateChangeMessage.h>
|
||||
#import <SignalUtilitiesKit/TSCall.h>
|
||||
#import <SignalUtilitiesKit/TSErrorMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
|
@ -290,17 +289,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
: [UIImage imageNamed:@"system_message_disappearing_messages_disabled"]);
|
||||
break;
|
||||
}
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
OWSAssertDebug([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]);
|
||||
if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction;
|
||||
BOOL isVerified = message.verificationState == OWSVerificationStateVerified;
|
||||
if (!isVerified) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
result = [UIImage imageNamed:@"system_message_verified"];
|
||||
break;
|
||||
}
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
result = [UIImage imageNamed:@"system_message_call"];
|
||||
|
@ -398,8 +386,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
return [self actionForErrorMessage:(TSErrorMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
||||
return [self actionForInfoMessage:(TSInfoMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
return [self actionForCall:(TSCall *)interaction];
|
||||
} else {
|
||||
OWSFailDebug(@"Tap for system messages of unknown type: %@", [interaction class]);
|
||||
return nil;
|
||||
|
@ -414,21 +400,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
switch (message.errorType) {
|
||||
case TSErrorMessageInvalidKeyException:
|
||||
return nil;
|
||||
case TSErrorMessageNonBlockingIdentityChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
|
||||
}];
|
||||
case TSErrorMessageWrongTrustedIdentityKey:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
|
||||
}];
|
||||
case TSErrorMessageMissingKeyId:
|
||||
case TSErrorMessageNoSession:
|
||||
case TSErrorMessageInvalidMessage:
|
||||
|
@ -486,48 +457,12 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
block:^{
|
||||
[weakSelf.delegate showConversationSettings];
|
||||
}];
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message)
|
||||
.recipientId];
|
||||
}];
|
||||
}
|
||||
|
||||
OWSLogInfo(@"Unhandled tap for info message: %@", message);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable SystemMessageAction *)actionForCall:(TSCall *)call
|
||||
{
|
||||
OWSAssertDebug(call);
|
||||
|
||||
__weak OWSSystemMessageCell *weakSelf = self;
|
||||
switch (call.callType) {
|
||||
case RPRecentCallTypeIncoming:
|
||||
case RPRecentCallTypeIncomingMissed:
|
||||
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
|
||||
case RPRecentCallTypeIncomingDeclined:
|
||||
return
|
||||
[SystemMessageAction actionWithTitle:NSLocalizedString(@"CALLBACK_BUTTON_TITLE", @"notification action")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoing:
|
||||
case RPRecentCallTypeOutgoingMissed:
|
||||
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALL_AGAIN_BUTTON_TITLE",
|
||||
@"Label for button that lets users call a contact again.")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoingIncomplete:
|
||||
case RPRecentCallTypeIncomingIncomplete:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
|
||||
|
|
|
@ -19,7 +19,7 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
private let kAvatarSize: CGFloat = 36
|
||||
private let kAvatarHSpacing: CGFloat = 8
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let bubbleView = OWSBubbleView()
|
||||
private let typingIndicatorView = TypingIndicatorView()
|
||||
private var viewConstraints = [NSLayoutConstraint]()
|
||||
|
@ -39,8 +39,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
bubbleView.addSubview(typingIndicatorView)
|
||||
contentView.addSubview(bubbleView)
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -65,16 +65,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
typingIndicatorView.autoPinBottomToSuperviewMargin(withInset: conversationStyle.textInsetBottom)
|
||||
])
|
||||
|
||||
if let avatarView = configureAvatarView() {
|
||||
contentView.addSubview(avatarView)
|
||||
viewConstraints.append(contentsOf: [
|
||||
bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
])
|
||||
|
||||
} else {
|
||||
avatarView.removeFromSuperview()
|
||||
}
|
||||
// if let avatarView = configureAvatarView() {
|
||||
// contentView.addSubview(avatarView)
|
||||
// viewConstraints.append(contentsOf: [
|
||||
// bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
// bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
// ])
|
||||
//
|
||||
// } else {
|
||||
// avatarView.removeFromSuperview()
|
||||
// }
|
||||
}
|
||||
|
||||
private func configureAvatarView() -> UIView? {
|
||||
|
@ -93,15 +93,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
owsFailDebug("Missing authorConversationColorName")
|
||||
return nil
|
||||
}
|
||||
guard let authorAvatarImage =
|
||||
OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
colorName: ConversationColorName(rawValue: colorName),
|
||||
diameter: UInt(kAvatarSize)).build() else {
|
||||
owsFailDebug("Could build avatar image")
|
||||
return nil
|
||||
}
|
||||
avatarView.image = authorAvatarImage
|
||||
return avatarView
|
||||
// guard let authorAvatarImage =
|
||||
// OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
// colorName: ConversationColorName(rawValue: colorName),
|
||||
// diameter: UInt(kAvatarSize)).build() else {
|
||||
// owsFailDebug("Could build avatar image")
|
||||
// return nil
|
||||
// }
|
||||
// avatarView.image = authorAvatarImage
|
||||
// return avatarView
|
||||
return UIView()
|
||||
}
|
||||
|
||||
private func shouldShowAvatar() -> Bool {
|
||||
|
@ -140,8 +141,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
NSLayoutConstraint.deactivate(viewConstraints)
|
||||
viewConstraints = [NSLayoutConstraint]()
|
||||
|
||||
avatarView.image = nil
|
||||
avatarView.removeFromSuperview()
|
||||
// avatarView.image = nil
|
||||
// avatarView.removeFromSuperview()
|
||||
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public protocol ConversationHeaderViewDelegate {
|
||||
func didTapConversationHeaderView(_ conversationHeaderView: ConversationHeaderView)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ConversationHeaderView: UIStackView {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ConversationHeaderViewDelegate?
|
||||
|
||||
@objc
|
||||
public var attributedTitle: NSAttributedString? {
|
||||
get {
|
||||
return self.titleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.titleLabel.attributedText = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public var attributedSubtitle: NSAttributedString? {
|
||||
get {
|
||||
return self.subtitleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.subtitleLabel.attributedText = newValue
|
||||
self.subtitleLabel.isHidden = newValue == nil
|
||||
}
|
||||
}
|
||||
|
||||
public var avatarImage: UIImage? {
|
||||
get {
|
||||
return self.avatarView.image
|
||||
}
|
||||
set {
|
||||
self.avatarView.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public let titlePrimaryFont: UIFont = UIFont.ows_boldFont(withSize: 17)
|
||||
@objc
|
||||
public let titleSecondaryFont: UIFont = UIFont.ows_regularFont(withSize: 9)
|
||||
@objc
|
||||
public let subtitleFont: UIFont = UIFont.ows_regularFont(withSize: 12)
|
||||
|
||||
private let titleLabel: UILabel
|
||||
private let subtitleLabel: UILabel
|
||||
private let avatarView: ConversationAvatarImageView
|
||||
|
||||
@objc
|
||||
public required init(thread: TSThread, contactsManager: OWSContactsManager) {
|
||||
|
||||
let avatarView = ConversationAvatarImageView(thread: thread, diameter: 36, contactsManager: contactsManager)
|
||||
self.avatarView = avatarView
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.navbarTitleColor
|
||||
titleLabel.lineBreakMode = .byTruncatingTail
|
||||
titleLabel.font = titlePrimaryFont
|
||||
titleLabel.setContentHuggingHigh()
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.textColor = Theme.navbarTitleColor
|
||||
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||
subtitleLabel.font = subtitleFont
|
||||
subtitleLabel.setContentHuggingHigh()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
textRows.distribution = .fillProportionally
|
||||
textRows.spacing = 0
|
||||
|
||||
textRows.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||
textRows.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
// low content hugging so that the text rows push container to the right bar button item(s)
|
||||
textRows.setContentHuggingLow()
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.layoutMargins = UIEdgeInsets(top: 4, left: 2, bottom: 4, right: 2)
|
||||
self.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
self.axis = .horizontal
|
||||
self.alignment = .center
|
||||
self.spacing = 0
|
||||
self.addArrangedSubview(avatarView)
|
||||
self.addArrangedSubview(textRows)
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
required public override init(frame: CGRect) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
// Grow to fill as much of the navbar as possible.
|
||||
return UIView.layoutFittingExpandedSize
|
||||
}
|
||||
|
||||
@objc
|
||||
public func updateAvatar() {
|
||||
self.avatarView.updateImage()
|
||||
}
|
||||
|
||||
// MARK: Delegate Methods
|
||||
|
||||
@objc func didTapView(tapGesture: UITapGestureRecognizer) {
|
||||
guard tapGesture.state == .recognized else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.didTapConversationHeaderView(self)
|
||||
}
|
||||
}
|
|
@ -5,17 +5,14 @@
|
|||
#import "ConversationInputToolbar.h"
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Environment.h"
|
||||
#import "OWSContactsManager.h"
|
||||
#import "OWSMath.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/TSQuotedMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,6 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) {
|
|||
OWSMessageCellType_TextOnlyMessage,
|
||||
OWSMessageCellType_Audio,
|
||||
OWSMessageCellType_GenericAttachment,
|
||||
OWSMessageCellType_ContactShare,
|
||||
OWSMessageCellType_MediaMessage,
|
||||
OWSMessageCellType_OversizeTextDownloading,
|
||||
};
|
||||
|
@ -67,7 +66,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
@property (nonatomic, readonly, nullable) OWSQuotedReplyModel *quotedReply;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isGroupThread;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
@property (nonatomic, readonly) BOOL userCanDeleteGroupMessage;
|
||||
|
||||
@property (nonatomic, readonly) BOOL hasBodyText;
|
||||
|
@ -163,7 +161,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import "ConversationViewItem.h"
|
||||
#import "OWSContactOffersCell.h"
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "OWSSystemMessageCell.h"
|
||||
|
@ -13,7 +13,7 @@
|
|||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
#import <SignalUtilitiesKit/TSInteraction.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
@ -31,8 +31,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
return @"OWSMessageCellType_GenericAttachment";
|
||||
case OWSMessageCellType_Unknown:
|
||||
return @"OWSMessageCellType_Unknown";
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return @"OWSMessageCellType_ContactShare";
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
return @"OWSMessageCellType_MediaMessage";
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
|
@ -119,7 +117,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
@synthesize interaction = _interaction;
|
||||
@synthesize isFirstInCluster = _isFirstInCluster;
|
||||
@synthesize isGroupThread = _isGroupThread;
|
||||
@synthesize isRSSFeed = _isRSSFeed;
|
||||
@synthesize isLastInCluster = _isLastInCluster;
|
||||
@synthesize lastAudioMessageView = _lastAudioMessageView;
|
||||
@synthesize senderName = _senderName;
|
||||
|
@ -127,7 +124,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
|
@ -143,11 +139,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
_interaction = interaction;
|
||||
_isGroupThread = isGroupThread;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_conversationStyle = conversationStyle;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
|
||||
return self;
|
||||
|
@ -173,37 +166,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.linkPreview = nil;
|
||||
self.linkPreviewAttachment = nil;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self clearCachedLayoutState];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
}
|
||||
|
||||
- (void)updateAuthorConversationColorNameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_TypingIndicator: {
|
||||
OWSTypingIndicatorInteraction *typingIndicator = (OWSTypingIndicatorInteraction *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:typingIndicator.recipientId
|
||||
transaction:transaction];
|
||||
break;
|
||||
}
|
||||
case OWSInteractionType_IncomingMessage: {
|
||||
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:incomingMessage.authorId transaction:transaction];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_authorConversationColorName = nil;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
|
@ -385,9 +352,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
measurementCell = [OWSSystemMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_Offer:
|
||||
measurementCell = [OWSContactOffersCell new];
|
||||
break;
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
measurementCell = [OWSTypingIndicatorCell new];
|
||||
break;
|
||||
|
@ -445,10 +409,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_Offer:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSTypingIndicatorCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
@ -609,12 +569,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.hasViewState = YES;
|
||||
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
if (message.contactShare) {
|
||||
self.contactShare =
|
||||
[[ContactShareViewModel alloc] initWithContactShareRecord:message.contactShare transaction:transaction];
|
||||
self.messageCellType = OWSMessageCellType_ContactShare;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for quoted replies _before_ media album handling,
|
||||
// since that logic may exit early.
|
||||
|
@ -804,35 +758,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
}
|
||||
case OWSInteractionType_Info: {
|
||||
TSInfoMessage *infoMessage = (TSInfoMessage *)self.interaction;
|
||||
if ([infoMessage isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *verificationMessage
|
||||
= (OWSVerificationStateChangeMessage *)infoMessage;
|
||||
BOOL isVerified = verificationMessage.verificationState == OWSVerificationStateVerified;
|
||||
NSString *displayName =
|
||||
[Environment.shared.contactsManager displayNameForPhoneIdentifier:verificationMessage.recipientId];
|
||||
NSString *titleFormat = (isVerified
|
||||
? (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"another device. Embeds {{user's name or phone number}}."))
|
||||
: (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"another device. Embeds {{user's name or phone number}}.")));
|
||||
return [NSString stringWithFormat:titleFormat, displayName];
|
||||
} else {
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
case OWSInteractionType_Call: {
|
||||
TSCall *call = (TSCall *)self.interaction;
|
||||
|
@ -921,11 +847,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
OWSFailDebug(@"No text to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
// TODO: Implement copy contact.
|
||||
OWSFailDebug(@"Not implemented yet");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
OWSFailDebug(@"Can't copy not-yet-downloaded attachment");
|
||||
return;
|
||||
|
@ -942,10 +863,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
OWSFailDebug(@"No media to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
[self copyAttachmentToPasteboard:self.attachmentStream];
|
||||
|
@ -996,9 +913,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"No media to share.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
|
@ -1035,8 +949,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1064,8 +976,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1104,9 +1014,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"Cannot save text data.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
OWSFailDebug(@"Cannot save media data.");
|
||||
break;
|
||||
|
@ -1177,7 +1084,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (self.isGroupThread) {
|
||||
// Skip if the thread is an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
|
@ -1217,8 +1123,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return self.attachmentStream != nil;
|
||||
|
@ -1252,7 +1156,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
// Ensure the thread is a public chat and not an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return false;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
|
|
|
@ -98,12 +98,10 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
|||
@property (nonatomic, readonly) ConversationViewState *viewState;
|
||||
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
|
||||
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary;
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
|
@ -226,7 +226,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
@ -242,7 +241,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
_persistedViewItems = @[];
|
||||
_unsavedOutgoingMessages = @[];
|
||||
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_viewState = [[ConversationViewState alloc] initWithViewItems:@[]];
|
||||
|
||||
[self configure];
|
||||
|
@ -269,11 +267,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return self.primaryStorage.dbReadWriteConnection;
|
||||
}
|
||||
|
||||
- (OWSContactsManager *)contactsManager
|
||||
{
|
||||
return (OWSContactsManager *)SSKEnvironment.shared.contactsManager;
|
||||
}
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
|
@ -304,10 +297,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
selector:@selector(applicationDidEnterBackground:)
|
||||
name:OWSApplicationDidEnterBackgroundNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(signalAccountsDidChange:)
|
||||
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(typingIndicatorStateDidChange:)
|
||||
name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange]
|
||||
|
@ -530,7 +519,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
ThreadDynamicInteractions *dynamicInteractions =
|
||||
[ThreadUtil ensureDynamicInteractionsForThread:self.thread
|
||||
contactsManager:self.contactsManager
|
||||
blockingManager:self.blockingManager
|
||||
dbConnection:self.editingDatabaseConnection
|
||||
hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator
|
||||
|
@ -1023,24 +1011,10 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return;
|
||||
}
|
||||
|
||||
// Many OWSProfileManager methods aren't safe to call from inside a database
|
||||
// transaction, so do this work now.
|
||||
//
|
||||
// TODO: It'd be nice if these methods took a transaction.
|
||||
BOOL hasLocalProfile = [self.profileManager hasLocalProfile];
|
||||
BOOL isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread];
|
||||
BOOL hasUnwhitelistedMember = NO;
|
||||
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
||||
if (![self.profileManager isUserInProfileWhitelist:recipientId]) {
|
||||
hasUnwhitelistedMember = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ConversationProfileState *conversationProfileState = [ConversationProfileState new];
|
||||
conversationProfileState.hasLocalProfile = hasLocalProfile;
|
||||
conversationProfileState.isThreadInProfileWhitelist = isThreadInProfileWhitelist;
|
||||
conversationProfileState.hasUnwhitelistedMember = hasUnwhitelistedMember;
|
||||
conversationProfileState.hasLocalProfile = YES;
|
||||
conversationProfileState.isThreadInProfileWhitelist = YES;
|
||||
conversationProfileState.hasUnwhitelistedMember = NO;
|
||||
self.conversationProfileState = conversationProfileState;
|
||||
}
|
||||
|
||||
|
@ -1136,15 +1110,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
// Don't create profile whitelist offers for users which are not already blocked.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
|
||||
if ([self.contactsManager hasSignalAccountForRecipientId:recipientId]) {
|
||||
// Only create "add to contacts" offers for non-contacts.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Only create block offers for non-contacts.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't create profile whitelist offers for non-contacts.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTooManyOutgoingMessagesToBlock) {
|
||||
|
@ -1214,7 +1179,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
NSArray<NSString *> *loadedUniqueIds = [self.messageMapping loadedUniqueIds];
|
||||
BOOL isGroupThread = self.thread.isGroupThread;
|
||||
BOOL isRSSFeed = self.isRSSFeed;
|
||||
ConversationStyle *conversationStyle = self.delegate.conversationStyle;
|
||||
|
||||
[self ensureConversationProfileState];
|
||||
|
@ -1228,7 +1192,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
if (!viewItem) {
|
||||
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction
|
||||
isGroupThread:isGroupThread
|
||||
isRSSFeed:isRSSFeed
|
||||
transaction:transaction
|
||||
conversationStyle:conversationStyle];
|
||||
}
|
||||
|
@ -1236,7 +1199,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
viewItemCache[interaction.uniqueId] = viewItem;
|
||||
[viewItems addObject:viewItem];
|
||||
TSMessage *message = (TSMessage *)viewItem.interaction;
|
||||
if (message.hasAttachmentsInNSE) {
|
||||
if (message.hasUnfetchedAttachmentsFromPN) {
|
||||
[SSKEnvironment.shared.attachmentDownloads downloadAttachmentsForMessage:message
|
||||
transaction:transaction
|
||||
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
|
||||
|
@ -1534,8 +1497,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
}
|
||||
|
||||
if (shouldShowSenderName) {
|
||||
senderName = [self.contactsManager attributedContactOrProfileNameForPhoneIdentifier:incomingSenderId primaryAttributes:[OWSMessageBubbleView senderNamePrimaryAttributes]
|
||||
secondaryAttributes:[OWSMessageBubbleView senderNameSecondaryAttributes]];
|
||||
senderName = [[NSAttributedString alloc] initWithString:[SSKEnvironment.shared.profileManager profileNameForRecipientWithID:incomingSenderId avoidingWriteTransaction:YES]];
|
||||
|
||||
if ([self.thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
||||
|
@ -1558,9 +1520,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
// the next message has the same sender avatar and
|
||||
// no "date break" separates us.
|
||||
shouldShowSenderAvatar = YES;
|
||||
if (viewItem.isRSSFeed) {
|
||||
shouldShowSenderAvatar = NO;
|
||||
} else if (previousViewItem && previousViewItem.interaction.interactionType == interactionType) {
|
||||
if (previousViewItem && previousViewItem.interaction.interactionType == interactionType) {
|
||||
shouldShowSenderAvatar = (![NSObject isNullableObject:previousIncomingSenderId equalTo:incomingSenderId]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface DomainFrontingCountryViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface DomainFrontingCountryViewController ()
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation DomainFrontingCountryViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(
|
||||
@"CENSORSHIP_CIRCUMVENTION_COUNTRY_VIEW_TITLE", @"Title for the 'censorship circumvention country' view.");
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
NSString *currentCountryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
|
||||
__weak DomainFrontingCountryViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
section.headerTitle = NSLocalizedString(
|
||||
@"DOMAIN_FRONTING_COUNTRY_VIEW_SECTION_HEADER", @"Section title for the 'domain fronting country' view.");
|
||||
for (OWSCountryMetadata *countryMetadata in [OWSCountryMetadata allCountryMetadatas]) {
|
||||
[section addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UITableViewCell *cell = [OWSTableItem newCell];
|
||||
[OWSTableItem configureCell:cell];
|
||||
cell.textLabel.text = countryMetadata.localizedCountryName;
|
||||
|
||||
if ([countryMetadata.countryCode isEqualToString:currentCountryCode]) {
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf selectCountry:countryMetadata];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:section];
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)selectCountry:(OWSCountryMetadata *)countryMetadata
|
||||
{
|
||||
OWSAssertDebug(countryMetadata);
|
||||
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryMetadata.countryCode;
|
||||
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewController : OWSViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,558 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewController.h"
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSBezierPathView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^CustomLayoutBlock)(void);
|
||||
|
||||
@interface CustomLayoutView : UIView
|
||||
|
||||
@property (nonatomic) CustomLayoutBlock layoutBlock;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation CustomLayoutView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.layoutBlock();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface FingerprintViewController () <OWSCompareSafetyNumbersActivityDelegate>
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *shareButton;
|
||||
|
||||
@property (nonatomic) UILabel *verificationStateLabel;
|
||||
@property (nonatomic) UILabel *verifyUnverifyButtonLabel;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
if (!recipientIdentity) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_TITLE",
|
||||
@"Title for alert explaining that a user cannot be verified.")
|
||||
message:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_MESSAGE",
|
||||
@"Message for alert explaining that a user cannot be verified.")];
|
||||
return;
|
||||
}
|
||||
|
||||
FingerprintViewController *fingerprintViewController = [FingerprintViewController new];
|
||||
[fingerprintViewController configureWithRecipientId:recipientId];
|
||||
OWSNavigationController *navigationController =
|
||||
[[OWSNavigationController alloc] initWithRootViewController:fingerprintViewController];
|
||||
[viewController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(identityStateDidChange:)
|
||||
name:kNSNotificationName_IdentityStateDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"PRIVACY_VERIFICATION_TITLE", @"Navbar title");
|
||||
|
||||
self.navigationItem.leftBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
||||
target:self
|
||||
action:@selector(closeButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"stop")];
|
||||
self.shareButton =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
||||
target:self
|
||||
action:@selector(didTapShareButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"share")];
|
||||
self.navigationItem.rightBarButtonItem = self.shareButton;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
// Verify/Unverify Button
|
||||
UIView *verifyUnverifyButton = [UIView new];
|
||||
[verifyUnverifyButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(verifyUnverifyButtonTapped:)]];
|
||||
[self.view addSubview:verifyUnverifyButton];
|
||||
[verifyUnverifyButton autoPinWidthToSuperview];
|
||||
[verifyUnverifyButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, verifyUnverifyButton);
|
||||
|
||||
UIView *verifyUnverifyPillbox = [UIView new];
|
||||
verifyUnverifyPillbox.backgroundColor = [UIColor ows_materialBlueColor];
|
||||
verifyUnverifyPillbox.layer.cornerRadius = 3.f;
|
||||
verifyUnverifyPillbox.clipsToBounds = YES;
|
||||
[verifyUnverifyButton addSubview:verifyUnverifyPillbox];
|
||||
[verifyUnverifyPillbox autoHCenterInSuperview];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(10.f, 20.f)];
|
||||
|
||||
UILabel *verifyUnverifyButtonLabel = [UILabel new];
|
||||
self.verifyUnverifyButtonLabel = verifyUnverifyButtonLabel;
|
||||
verifyUnverifyButtonLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 20.f)];
|
||||
verifyUnverifyButtonLabel.textColor = [UIColor whiteColor];
|
||||
verifyUnverifyButtonLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[verifyUnverifyPillbox addSubview:verifyUnverifyButtonLabel];
|
||||
[verifyUnverifyButtonLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 50.f)];
|
||||
[verifyUnverifyButtonLabel autoPinHeightToSuperviewWithMargin:ScaleFromIPhone5To7Plus(8.f, 8.f)];
|
||||
|
||||
// Learn More
|
||||
UIView *learnMoreButton = [UIView new];
|
||||
[learnMoreButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(learnMoreButtonTapped:)]];
|
||||
[self.view addSubview:learnMoreButton];
|
||||
[learnMoreButton autoPinWidthToSuperview];
|
||||
[learnMoreButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:verifyUnverifyButton withOffset:0];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, learnMoreButton);
|
||||
|
||||
UILabel *learnMoreLabel = [UILabel new];
|
||||
learnMoreLabel.attributedText = [[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_SAFETY_NUMBERS_LEARN_MORE",
|
||||
@"Label for a link to more information about safety numbers and verification.")
|
||||
attributes:@{
|
||||
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid),
|
||||
}];
|
||||
learnMoreLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 16.f)];
|
||||
learnMoreLabel.textColor = [UIColor ows_materialBlueColor];
|
||||
learnMoreLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[learnMoreButton addSubview:learnMoreLabel];
|
||||
[learnMoreLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
|
||||
// Instructions
|
||||
NSString *instructionsFormat = NSLocalizedString(@"PRIVACY_VERIFICATION_INSTRUCTIONS",
|
||||
@"Paragraph(s) shown alongside the safety number when verifying privacy with {{contact name}}");
|
||||
UILabel *instructionsLabel = [UILabel new];
|
||||
instructionsLabel.text = [NSString stringWithFormat:instructionsFormat, self.contactName];
|
||||
instructionsLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 14.f)];
|
||||
instructionsLabel.textColor = Theme.secondaryColor;
|
||||
instructionsLabel.textAlignment = NSTextAlignmentCenter;
|
||||
instructionsLabel.numberOfLines = 0;
|
||||
instructionsLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:instructionsLabel];
|
||||
[instructionsLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[instructionsLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:learnMoreButton withOffset:0];
|
||||
|
||||
// Fingerprint Label
|
||||
UILabel *fingerprintLabel = [UILabel new];
|
||||
fingerprintLabel.text = self.fingerprint.displayableText;
|
||||
fingerprintLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:ScaleFromIPhone5To7Plus(20.f, 23.f)];
|
||||
fingerprintLabel.textColor = Theme.secondaryColor;
|
||||
fingerprintLabel.numberOfLines = 3;
|
||||
fingerprintLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
fingerprintLabel.adjustsFontSizeToFitWidth = YES;
|
||||
[fingerprintLabel
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintLabelTapped:)]];
|
||||
fingerprintLabel.userInteractionEnabled = YES;
|
||||
[self.view addSubview:fingerprintLabel];
|
||||
[fingerprintLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 60.f)];
|
||||
[fingerprintLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:instructionsLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(8.f, 15.f)];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintLabel);
|
||||
|
||||
// Fingerprint Image
|
||||
CustomLayoutView *fingerprintView = [CustomLayoutView new];
|
||||
[self.view addSubview:fingerprintView];
|
||||
[fingerprintView autoPinWidthToSuperview];
|
||||
[fingerprintView autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[fingerprintView
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintViewTapped:)]];
|
||||
fingerprintView.userInteractionEnabled = YES;
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintView);
|
||||
|
||||
OWSBezierPathView *fingerprintCircle = [OWSBezierPathView new];
|
||||
[fingerprintCircle setConfigureShapeLayerBlock:^(CAShapeLayer *layer, CGRect bounds) {
|
||||
layer.fillColor = Theme.offBackgroundColor.CGColor;
|
||||
CGFloat size = MIN(bounds.size.width, bounds.size.height);
|
||||
CGRect circle = CGRectMake((bounds.size.width - size) * 0.5f, (bounds.size.height - size) * 0.5f, size, size);
|
||||
layer.path = [UIBezierPath bezierPathWithOvalInRect:circle].CGPath;
|
||||
}];
|
||||
[fingerprintView addSubview:fingerprintCircle];
|
||||
[fingerprintCircle ows_autoPinToSuperviewEdges];
|
||||
|
||||
UIImageView *fingerprintImageView = [UIImageView new];
|
||||
fingerprintImageView.image = self.fingerprint.image;
|
||||
// Don't antialias QR Codes.
|
||||
fingerprintImageView.layer.magnificationFilter = kCAFilterNearest;
|
||||
fingerprintImageView.layer.minificationFilter = kCAFilterNearest;
|
||||
[fingerprintView addSubview:fingerprintImageView];
|
||||
|
||||
UILabel *scanLabel = [UILabel new];
|
||||
scanLabel.text = NSLocalizedString(@"PRIVACY_TAP_TO_SCAN", @"Button that shows the 'scan with camera' view.");
|
||||
scanLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
scanLabel.textColor = Theme.secondaryColor;
|
||||
[scanLabel sizeToFit];
|
||||
[fingerprintView addSubview:scanLabel];
|
||||
|
||||
fingerprintView.layoutBlock = ^{
|
||||
CGFloat size = round(MIN(fingerprintView.width, fingerprintView.height) * 0.675f);
|
||||
fingerprintImageView.frame = CGRectMake(
|
||||
round((fingerprintView.width - size) * 0.5f), round((fingerprintView.height - size) * 0.5f), size, size);
|
||||
CGFloat scanY = round(fingerprintImageView.bottom
|
||||
+ ((fingerprintView.height - fingerprintImageView.bottom) - scanLabel.height) * 0.33f);
|
||||
scanLabel.frame = CGRectMake(
|
||||
round((fingerprintView.width - scanLabel.width) * 0.5f), scanY, scanLabel.width, scanLabel.height);
|
||||
};
|
||||
|
||||
// Verification State
|
||||
UILabel *verificationStateLabel = [UILabel new];
|
||||
self.verificationStateLabel = verificationStateLabel;
|
||||
verificationStateLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(16.f, 20.f)];
|
||||
verificationStateLabel.textColor = Theme.secondaryColor;
|
||||
verificationStateLabel.textAlignment = NSTextAlignmentCenter;
|
||||
verificationStateLabel.numberOfLines = 0;
|
||||
verificationStateLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:verificationStateLabel];
|
||||
[verificationStateLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
// Bind height of label to height of two lines of text.
|
||||
// This should always be sufficient, and will prevent the view's
|
||||
// layout from changing if the user is marked as verified or not
|
||||
// verified.
|
||||
[verificationStateLabel autoSetDimension:ALDimensionHeight
|
||||
toSize:round(verificationStateLabel.font.lineHeight * 2.25f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:ScaleFromIPhone5To7Plus(15.f, 20.f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintView
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
- (void)updateVerificationStateLabel
|
||||
{
|
||||
OWSAssertDebug(self.recipientId.length > 0);
|
||||
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
if (isVerified) {
|
||||
NSMutableAttributedString *labelText = [NSMutableAttributedString new];
|
||||
|
||||
if (isVerified) {
|
||||
// Show a "checkmark" if this user is verified.
|
||||
[labelText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verificationStateLabel.font.pointSize],
|
||||
}]];
|
||||
}
|
||||
|
||||
[labelText
|
||||
appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is verified. Embeds "
|
||||
@"{{the user's name or phone number}}."),
|
||||
self.contactName]]];
|
||||
self.verificationStateLabel.attributedText = labelText;
|
||||
|
||||
self.verifyUnverifyButtonLabel.text = NSLocalizedString(
|
||||
@"PRIVACY_UNVERIFY_BUTTON", @"Button that lets user mark another user's identity as unverified.");
|
||||
} else {
|
||||
self.verificationStateLabel.text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_NOT_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is not verified. Embeds {{the user's name or phone "
|
||||
@"number}}."),
|
||||
self.contactName];
|
||||
|
||||
NSMutableAttributedString *buttonText = [NSMutableAttributedString new];
|
||||
// Show a "checkmark" if this user is not verified.
|
||||
[buttonText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verifyUnverifyButtonLabel.font.pointSize],
|
||||
}]];
|
||||
[buttonText appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_VERIFY_BUTTON",
|
||||
@"Button that lets user mark another user's identity as verified.")]];
|
||||
self.verifyUnverifyButtonLabel.attributedText = buttonText;
|
||||
}
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)showSharingActivityWithCompletion:(nullable void (^)(void))completionHandler
|
||||
{
|
||||
OWSLogDebug(@"Sharing safety numbers");
|
||||
|
||||
OWSCompareSafetyNumbersActivity *compareActivity = [[OWSCompareSafetyNumbersActivity alloc] initWithDelegate:self];
|
||||
|
||||
NSString *shareFormat = NSLocalizedString(
|
||||
@"SAFETY_NUMBER_SHARE_FORMAT", @"Snippet to share {{safety number}} with a friend. sent e.g. via SMS");
|
||||
NSString *shareString = [NSString stringWithFormat:shareFormat, self.fingerprint.displayableText];
|
||||
|
||||
UIActivityViewController *activityController =
|
||||
[[UIActivityViewController alloc] initWithActivityItems:@[ shareString ]
|
||||
applicationActivities:@[ compareActivity ]];
|
||||
|
||||
activityController.completionWithItemsHandler = ^void(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
if (completionHandler) {
|
||||
completionHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
|
||||
NSString *const iCloudActivityType = @"com.apple.CloudDocsUI.AddToiCloudDrive";
|
||||
activityController.excludedActivityTypes = @[
|
||||
UIActivityTypePostToFacebook,
|
||||
UIActivityTypePostToWeibo,
|
||||
UIActivityTypeAirDrop,
|
||||
UIActivityTypePostToTwitter,
|
||||
iCloudActivityType // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
|
||||
];
|
||||
|
||||
[self presentViewController:activityController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - OWSCompareSafetyNumbersActivityDelegate
|
||||
|
||||
- (void)compareSafetyNumbersActivitySucceededWithActivity:(OWSCompareSafetyNumbersActivity *)activity
|
||||
{
|
||||
[self showVerificationSucceeded];
|
||||
}
|
||||
|
||||
- (void)compareSafetyNumbersActivity:(OWSCompareSafetyNumbersActivity *)activity failedWithError:(NSError *)error
|
||||
{
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[FingerprintViewScanController showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[FingerprintViewScanController showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:nil
|
||||
cancelBlock:^{
|
||||
// Do nothing.
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)closeButton
|
||||
{
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)didTapShareButton
|
||||
{
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)showScanner
|
||||
{
|
||||
FingerprintViewScanController *scanView = [FingerprintViewScanController new];
|
||||
[scanView configureWithRecipientId:self.recipientId];
|
||||
[self.navigationController pushViewController:scanView animated:YES];
|
||||
}
|
||||
|
||||
- (void)learnMoreButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
NSString *learnMoreURL = @"https://support.signal.org/hc/en-us/articles/"
|
||||
@"213134107";
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:learnMoreURL]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintLabelTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintViewTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showScanner];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)verifyUnverifyButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId
|
||||
transaction:transaction]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
OWSVerificationState newVerificationState
|
||||
= (isVerified ? OWSVerificationStateDefault : OWSVerificationStateVerified);
|
||||
[[OWSIdentityManager sharedManager] setVerificationState:newVerificationState
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
isUserInitiatedChange:YES
|
||||
transaction:transaction];
|
||||
}];
|
||||
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)identityStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController : OWSViewController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId NS_SWIFT_NAME(configure(recipientId:));
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag;
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,258 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController () <OWSQRScannerDelegate>
|
||||
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
@property (nonatomic) OWSQRCodeScanningViewController *qrScanningController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewScanController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
self.accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SCAN_QR_CODE_VIEW_TITLE", @"Title for the 'scan QR code' view.");
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = UIColor.blackColor;
|
||||
|
||||
self.qrScanningController = [OWSQRCodeScanningViewController new];
|
||||
self.qrScanningController.scanDelegate = self;
|
||||
[self.view addSubview:self.qrScanningController.view];
|
||||
[self.qrScanningController.view autoPinWidthToSuperview];
|
||||
[self.qrScanningController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
|
||||
UIView *footer = [UIView new];
|
||||
footer.backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f];
|
||||
[self.view addSubview:footer];
|
||||
[footer autoPinWidthToSuperview];
|
||||
[footer autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
[footer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.qrScanningController.view];
|
||||
|
||||
UILabel *cameraInstructionLabel = [UILabel new];
|
||||
cameraInstructionLabel.text
|
||||
= NSLocalizedString(@"SCAN_CODE_INSTRUCTIONS", @"label presented once scanning (camera) view is visible.");
|
||||
cameraInstructionLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 18.f)];
|
||||
cameraInstructionLabel.textColor = [UIColor whiteColor];
|
||||
cameraInstructionLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cameraInstructionLabel.numberOfLines = 0;
|
||||
cameraInstructionLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[footer addSubview:cameraInstructionLabel];
|
||||
[cameraInstructionLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(16.f, 30.f)];
|
||||
CGFloat instructionsVMargin = ScaleFromIPhone5To7Plus(10.f, 20.f);
|
||||
[cameraInstructionLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:instructionsVMargin];
|
||||
[cameraInstructionLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:instructionsVMargin];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self ows_askForCameraPermissions:^(BOOL granted) {
|
||||
if (granted) {
|
||||
// Camera stops capturing when "sharing" while in capture mode.
|
||||
// Also, it's less obvious whats being "shared" at this point,
|
||||
// so just disable sharing when in capture mode.
|
||||
|
||||
OWSLogInfo(@"Showing Scanner");
|
||||
|
||||
[self.qrScanningController startCapture];
|
||||
} else {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - OWSQRScannerDelegate
|
||||
|
||||
- (void)controller:(OWSQRCodeScanningViewController *)controller didDetectQRCodeWithData:(NSData *)data
|
||||
{
|
||||
[self verifyCombinedFingerprintData:data];
|
||||
}
|
||||
|
||||
- (void)verifyCombinedFingerprintData:(NSData *)combinedFingerprintData
|
||||
{
|
||||
NSError *error;
|
||||
if ([self.fingerprint matchesLogicalFingerprintsData:combinedFingerprintData error:&error]) {
|
||||
[self showVerificationSucceeded];
|
||||
} else {
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[self.class showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[self.class showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:^{
|
||||
[self.qrScanningController startCapture];
|
||||
}
|
||||
cancelBlock:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(identityKey.length > 0);
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug(contactName.length > 0);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Successfully verified safety numbers.", tag);
|
||||
|
||||
NSString *successTitle = NSLocalizedString(@"SUCCESSFUL_VERIFICATION_TITLE", nil);
|
||||
NSString *descriptionFormat = NSLocalizedString(
|
||||
@"SUCCESSFUL_VERIFICATION_DESCRIPTION", @"Alert body after verifying privacy with {{other user's name}}");
|
||||
NSString *successDescription = [NSString stringWithFormat:descriptionFormat, contactName];
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:successTitle
|
||||
message:successDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert
|
||||
addAction:[UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"FINGERPRINT_SCAN_VERIFY_BUTTON",
|
||||
@"Button that marks user as verified after a successful fingerprint scan.")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[OWSIdentityManager.sharedManager setVerificationState:OWSVerificationStateVerified
|
||||
identityKey:identityKey
|
||||
recipientId:recipientId
|
||||
isUserInitiatedChange:YES];
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}]];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}];
|
||||
[alert addAction:dismissAction];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
}
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(cancelBlock);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Failed to verify safety numbers.", tag);
|
||||
|
||||
NSString *_Nullable failureTitle;
|
||||
if (error.code != OWSErrorCodeUserError) {
|
||||
failureTitle = NSLocalizedString(@"FAILED_VERIFICATION_TITLE", @"alert title");
|
||||
} // else no title. We don't want to show a big scary "VERIFICATION FAILED" when it's just user error.
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:failureTitle
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
if (retryBlock) {
|
||||
[alert addAction:[UIAlertAction actionWithTitle:[CommonStrings retryButton]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
retryBlock();
|
||||
}]];
|
||||
}
|
||||
|
||||
[alert addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
|
||||
OWSLogWarn(@"%@ Identity verification failed with error: %@", tag, error);
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(nullable void (^)(void))completion
|
||||
{
|
||||
self.qrScanningController.view.hidden = YES;
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -35,7 +35,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
public weak var delegate: GifPickerViewControllerDelegate?
|
||||
|
||||
let thread: TSThread
|
||||
let messageSender: MessageSender
|
||||
|
||||
let searchBar: SearchBar
|
||||
let layout: GifPickerLayout
|
||||
|
@ -60,9 +59,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
}
|
||||
|
||||
@objc
|
||||
required init(thread: TSThread, messageSender: MessageSender) {
|
||||
required init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
self.messageSender = messageSender
|
||||
|
||||
self.searchBar = SearchBar()
|
||||
self.layout = GifPickerLayout()
|
||||
|
|
|
@ -7,15 +7,9 @@ import SignalUtilitiesKit
|
|||
|
||||
@objc class GroupTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
|
@ -30,14 +24,14 @@ import SignalUtilitiesKit
|
|||
|
||||
// Layout
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
avatarView.autoPinToSquareAspectRatio()
|
||||
// avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
// avatarView.autoPinToSquareAspectRatio()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [nameLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
|
||||
let columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
let columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.alignment = .center
|
||||
columns.spacing = kContactCellAvatarTextMargin
|
||||
|
@ -62,11 +56,11 @@ import SignalUtilitiesKit
|
|||
|
||||
let groupMemberIds: [String] = thread.groupModel.groupMemberIds
|
||||
let groupMemberNames = groupMemberIds.map { (recipientId: String) in
|
||||
contactsManager.displayName(forPhoneIdentifier: recipientId)
|
||||
SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: recipientId, avoidingWriteTransaction: true)!
|
||||
}.joined(separator: ", ")
|
||||
self.subtitleLabel.text = groupMemberNames
|
||||
|
||||
self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
// self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Social
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSInviteFlow)
|
||||
class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate, ContactsPickerDelegate {
|
||||
enum Channel {
|
||||
case message, mail, twitter
|
||||
}
|
||||
|
||||
let installUrl = "https://signal.org/install/"
|
||||
let homepageUrl = "https://signal.org"
|
||||
|
||||
@objc
|
||||
let actionSheetController: UIAlertController
|
||||
|
||||
@objc
|
||||
let presentingViewController: UIViewController
|
||||
|
||||
var channel: Channel?
|
||||
|
||||
@objc
|
||||
required init(presentingViewController: UIViewController) {
|
||||
self.presentingViewController = presentingViewController
|
||||
actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
super.init()
|
||||
|
||||
actionSheetController.addAction(dismissAction())
|
||||
|
||||
if let messageAction = messageAction() {
|
||||
actionSheetController.addAction(messageAction)
|
||||
}
|
||||
|
||||
if let mailAction = mailAction() {
|
||||
actionSheetController.addAction(mailAction)
|
||||
}
|
||||
|
||||
if let tweetAction = tweetAction() {
|
||||
actionSheetController.addAction(tweetAction)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
Logger.verbose("[InviteFlow] deinit")
|
||||
}
|
||||
|
||||
// MARK: Twitter
|
||||
|
||||
func canTweet() -> Bool {
|
||||
return SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter)
|
||||
}
|
||||
|
||||
func tweetAction() -> UIAlertAction? {
|
||||
guard canTweet() else {
|
||||
Logger.info("Twitter not supported.")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let twitterViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter) else {
|
||||
Logger.error("unable to build twitter controller.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let tweetString = NSLocalizedString("SETTINGS_INVITE_TWITTER_TEXT", comment: "content of tweet when inviting via twitter - please do not translate URL")
|
||||
twitterViewController.setInitialText(tweetString)
|
||||
|
||||
let tweetUrl = URL(string: installUrl)
|
||||
twitterViewController.add(tweetUrl)
|
||||
twitterViewController.add(#imageLiteral(resourceName: "twitter_sharing_image"))
|
||||
|
||||
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment: "action sheet item")
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { _ in
|
||||
Logger.debug("Chose tweet")
|
||||
|
||||
self.presentingViewController.present(twitterViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: CommonStrings.dismissButton, style: .cancel)
|
||||
}
|
||||
|
||||
// MARK: ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("didSelectContacts:\(contacts)")
|
||||
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel after returning from contact picker.")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
|
||||
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
|
||||
case .mail:
|
||||
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
|
||||
sendMailTo(emails: recipients)
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel in contact picker.")
|
||||
return true
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
return contact.userTextPhoneNumbers.count > 0
|
||||
case .mail:
|
||||
return contact.emails.count > 0
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
Logger.error("with error: \(error)")
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("ERROR_COULD_NOT_FETCH_CONTACTS", comment: "Error indicating that the phone's contacts could not be retrieved."))
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
||||
owsFailDebug("InviteFlow only supports multi-select")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: SMS
|
||||
|
||||
func messageAction() -> UIAlertAction? {
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
return nil
|
||||
}
|
||||
|
||||
let messageTitle = NSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
||||
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
||||
Logger.debug("Chose message.")
|
||||
self.channel = .message
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .phoneNumber)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func dismissAndSendSMSTo(phoneNumbers: [String]) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
if phoneNumbers.count > 1 {
|
||||
let warning = UIAlertController(title: nil,
|
||||
message: NSLocalizedString("INVITE_WARNING_MULTIPLE_INVITES_BY_TEXT",
|
||||
comment: "Alert warning that sending an invite to multiple users will create a group message whose recipients will be able to see each other."),
|
||||
preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_CONTINUE",
|
||||
comment: "Label for 'continue' button."),
|
||||
style: .default, handler: { _ in
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}))
|
||||
warning.addAction(OWSAlerts.cancelAction)
|
||||
self.presentingViewController.presentAlert(warning)
|
||||
} else {
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func sendSMSTo(phoneNumbers: [String]) {
|
||||
let messageComposeViewController = MFMessageComposeViewController()
|
||||
messageComposeViewController.messageComposeDelegate = self
|
||||
messageComposeViewController.recipients = phoneNumbers
|
||||
|
||||
let inviteText = NSLocalizedString("SMS_INVITE_BODY", comment: "body sent to contacts when inviting to Install Signal")
|
||||
messageComposeViewController.body = inviteText.appending(" \(self.installUrl)")
|
||||
self.presentingViewController.present(messageComposeViewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: MessageComposeViewControllerDelegate
|
||||
|
||||
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via SMS.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled message invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mail
|
||||
|
||||
func mailAction() -> UIAlertAction? {
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
Logger.info("Device cannot send mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
let mailActionTitle = NSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { _ in
|
||||
Logger.debug("Chose mail.")
|
||||
self.channel = .mail
|
||||
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .email)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMailTo(emails recipientEmails: [String]) {
|
||||
let mailComposeViewController = MFMailComposeViewController()
|
||||
mailComposeViewController.mailComposeDelegate = self
|
||||
mailComposeViewController.setBccRecipients(recipientEmails)
|
||||
|
||||
let subject = NSLocalizedString("EMAIL_INVITE_SUBJECT", comment: "subject of email sent to contacts when inviting to install Signal")
|
||||
let bodyFormat = NSLocalizedString("EMAIL_INVITE_BODY", comment: "body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to the Signal home page}}")
|
||||
let body = String.init(format: bodyFormat, installUrl, homepageUrl)
|
||||
mailComposeViewController.setSubject(subject)
|
||||
mailComposeViewController.setMessageBody(body, isHTML: false)
|
||||
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
self.presentingViewController.present(mailComposeViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MailComposeViewControllerDelegate
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via mail.")
|
||||
case .saved:
|
||||
Logger.debug("user saved mail invite.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled mail invite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -176,7 +176,7 @@ extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
}
|
||||
|
||||
let checkForCancel = category == .incomingMessage
|
||||
if checkForCancel && hasReceivedSyncMessageRecently {
|
||||
if checkForCancel {
|
||||
assert(userInfo[AppNotificationUserInfoKey.threadId] != nil)
|
||||
notification.fireDate = Date(timeIntervalSinceNow: kNotificationDelayForRemoteRead)
|
||||
notification.timeZone = NSTimeZone.local
|
||||
|
|
|
@ -587,7 +587,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
let conversationStyle = ConversationStyle(thread: thread)
|
||||
fetchedItem = ConversationInteractionViewItem(interaction: message,
|
||||
isGroupThread: thread.isGroupThread(),
|
||||
isRSSFeed: false,
|
||||
transaction: transaction,
|
||||
conversationStyle: conversationStyle)
|
||||
}
|
||||
|
@ -670,10 +669,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
// MARK: Dynamic Header
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
private func senderName(message: TSMessage) -> String {
|
||||
switch message {
|
||||
case let incomingMessage as TSIncomingMessage:
|
||||
|
|
|
@ -93,9 +93,8 @@ class ConversationViewItemActions: NSObject {
|
|||
var actions: [MenuAction] = []
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if shouldAllowReply && !isRSSFeed {
|
||||
if shouldAllowReply {
|
||||
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(replyAction)
|
||||
}
|
||||
|
@ -105,7 +104,7 @@ class ConversationViewItemActions: NSObject {
|
|||
actions.append(copyTextAction)
|
||||
}
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
@ -132,9 +131,8 @@ class ConversationViewItemActions: NSObject {
|
|||
var actions: [MenuAction] = []
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if shouldAllowReply && !isRSSFeed {
|
||||
if shouldAllowReply {
|
||||
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(replyAction)
|
||||
}
|
||||
|
@ -150,7 +148,7 @@ class ConversationViewItemActions: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
@ -182,9 +180,8 @@ class ConversationViewItemActions: NSObject {
|
|||
}
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ protocol MessageDetailViewDelegate: AnyObject {
|
|||
}
|
||||
|
||||
@objc
|
||||
class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate, ContactShareViewHelperDelegate {
|
||||
class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate {
|
||||
|
||||
@objc
|
||||
weak var delegate: MessageDetailViewDelegate?
|
||||
|
@ -52,18 +52,12 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
|
||||
var conversationStyle: ConversationStyle
|
||||
|
||||
private var contactShareViewHelper: ContactShareViewHelper!
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
||||
var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use other constructor instead.")
|
||||
|
@ -86,8 +80,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager)
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
do {
|
||||
try updateMessageToLatest()
|
||||
|
@ -614,27 +606,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
|
||||
}
|
||||
|
||||
func didTapContactShare(_ viewItem: ConversationViewItem) {
|
||||
guard let contactShare = viewItem.contactShare else {
|
||||
owsFailDebug("missing contact.")
|
||||
return
|
||||
}
|
||||
let contactViewController = ContactViewController(contactShare: contactShare)
|
||||
self.navigationController?.pushViewController(contactViewController, animated: true)
|
||||
}
|
||||
|
||||
func didTapSendMessage(toContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.sendMessage(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.showInviteContact(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didTapShowAddToContactUI(forContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.showAddToContacts(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
var audioAttachmentPlayer: OWSAudioPlayer?
|
||||
|
||||
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSMessageFetcherJob)
|
||||
public class MessageFetcherJob: NSObject {
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
SwiftSingletons.register(self)
|
||||
}
|
||||
|
||||
// MARK: Singletons
|
||||
|
||||
private var networkManager: TSNetworkManager {
|
||||
return SSKEnvironment.shared.networkManager
|
||||
}
|
||||
|
||||
private var messageReceiver: OWSMessageReceiver {
|
||||
return SSKEnvironment.shared.messageReceiver
|
||||
}
|
||||
|
||||
private var signalService: OWSSignalService {
|
||||
return OWSSignalService.sharedInstance()
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
@discardableResult
|
||||
public func run() -> Promise<Void> {
|
||||
let promise = fetchUndeliveredMessages().then { promises -> Promise<Void> in
|
||||
let promises = promises.map { promise -> Promise<Void> in
|
||||
return promise.then { envelopes -> Promise<Void> in
|
||||
for envelope in envelopes {
|
||||
Logger.info("Envelope received.")
|
||||
do {
|
||||
let envelopeData = try envelope.serializedData()
|
||||
self.messageReceiver.handleReceivedEnvelopeData(envelopeData)
|
||||
} catch {
|
||||
owsFailDebug("Failed to serialize envelope.")
|
||||
}
|
||||
self.acknowledgeDelivery(envelope: envelope)
|
||||
}
|
||||
return Promise.value(())
|
||||
}
|
||||
}
|
||||
return when(resolved: promises).asVoid()
|
||||
}
|
||||
promise.retainUntilComplete()
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
@discardableResult
|
||||
public func run() -> AnyPromise {
|
||||
return AnyPromise(run() as Promise)
|
||||
}
|
||||
|
||||
// use in DEBUG or wherever you can't receive push notifications to poll for messages.
|
||||
// Do not use in production.
|
||||
public func startRunLoop(timeInterval: Double) {
|
||||
Logger.error("Starting message fetch polling. This should not be used in production.")
|
||||
timer = WeakTimer.scheduledTimer(timeInterval: timeInterval, target: self, userInfo: nil, repeats: true) {[weak self] _ in
|
||||
let _: Promise<Void>? = self?.run()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRunLoop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func parseMessagesResponse(responseObject: Any?) -> (envelopes: [SSKProtoEnvelope], more: Bool)? {
|
||||
guard let responseObject = responseObject else {
|
||||
Logger.error("response object was surpringly nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let responseDict = responseObject as? [String: Any] else {
|
||||
Logger.error("response object was not a dictionary")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let messageDicts = responseDict["messages"] as? [[String: Any]] else {
|
||||
Logger.error("messages object was not a list of dictionaries")
|
||||
return nil
|
||||
}
|
||||
|
||||
let moreMessages = { () -> Bool in
|
||||
if let responseMore = responseDict["more"] as? Bool {
|
||||
return responseMore
|
||||
} else {
|
||||
Logger.warn("more object was not a bool. Assuming no more")
|
||||
return false
|
||||
}
|
||||
}()
|
||||
|
||||
let envelopes: [SSKProtoEnvelope] = messageDicts.compactMap { buildEnvelope(messageDict: $0) }
|
||||
|
||||
return (
|
||||
envelopes: envelopes,
|
||||
more: moreMessages
|
||||
)
|
||||
}
|
||||
|
||||
private func buildEnvelope(messageDict: [String: Any]) -> SSKProtoEnvelope? {
|
||||
do {
|
||||
let params = ParamParser(dictionary: messageDict)
|
||||
|
||||
let typeInt: Int32 = try params.required(key: "type")
|
||||
guard let type: SSKProtoEnvelope.SSKProtoEnvelopeType = SSKProtoEnvelope.SSKProtoEnvelopeType(rawValue: typeInt) else {
|
||||
Logger.error("`type` was invalid: \(typeInt)")
|
||||
throw ParamParser.ParseError.invalidFormat("type")
|
||||
}
|
||||
|
||||
guard let timestamp: UInt64 = try params.required(key: "timestamp") else {
|
||||
Logger.error("`timestamp` was invalid: \(typeInt)")
|
||||
throw ParamParser.ParseError.invalidFormat("timestamp")
|
||||
}
|
||||
|
||||
let builder = SSKProtoEnvelope.builder(type: type, timestamp: timestamp)
|
||||
|
||||
if let source: String = try params.optional(key: "source") {
|
||||
builder.setSource(source)
|
||||
}
|
||||
|
||||
if let sourceDevice: UInt32 = try params.optional(key: "sourceDevice") {
|
||||
builder.setSourceDevice(sourceDevice)
|
||||
}
|
||||
|
||||
if let legacyMessage = try params.optionalBase64EncodedData(key: "message") {
|
||||
builder.setLegacyMessage(legacyMessage)
|
||||
}
|
||||
if let content = try params.optionalBase64EncodedData(key: "content") {
|
||||
builder.setContent(content)
|
||||
}
|
||||
if let serverTimestamp: UInt64 = try params.optional(key: "serverTimestamp") {
|
||||
builder.setServerTimestamp(serverTimestamp)
|
||||
}
|
||||
if let serverGuid: String = try params.optional(key: "guid") {
|
||||
builder.setServerGuid(serverGuid)
|
||||
}
|
||||
|
||||
return try builder.build()
|
||||
} catch {
|
||||
owsFailDebug("error building envelope: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchUndeliveredMessages() -> Promise<Set<Promise<[SSKProtoEnvelope]>>> {
|
||||
let userPublickKey = getUserHexEncodedPublicKey() // Can be missing in rare cases
|
||||
guard !userPublickKey.isEmpty else { return Promise.value(Set()) }
|
||||
return SnodeAPI.getMessages(for: userPublickKey).map2 { promises -> Set<Promise<[SSKProtoEnvelope]>> in
|
||||
return Set(promises.map { promise -> Promise<[SSKProtoEnvelope]> in
|
||||
return promise.map2 { rawMessages -> [SSKProtoEnvelope] in
|
||||
return rawMessages.compactMap { SSKProtoEnvelope.from($0) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func acknowledgeDelivery(envelope: SSKProtoEnvelope) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -110,10 +110,6 @@ public class MessageRecipientStatusUtils: NSObject {
|
|||
// Use the "long" version of this message here.
|
||||
return (.failed, NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "status message for failed messages"))
|
||||
case .sending:
|
||||
if outgoingMessage.isCalculatingPoW {
|
||||
return (.calculatingPoW, NSLocalizedString("Calculating proof of work", comment: ""))
|
||||
}
|
||||
|
||||
if outgoingMessage.hasAttachments() {
|
||||
return (.uploading, NSLocalizedString("MESSAGE_STATUS_UPLOADING",
|
||||
comment: "status message while attachment is uploading"))
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSAddToContactViewController : OWSTableViewController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,212 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSAddToContactViewController.h"
|
||||
#import <SignalUtilitiesKit/ContactsViewHelper.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
|
||||
@import ContactsUI;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSAddToContactViewController () <ContactEditingDelegate, ContactsViewHelperDelegate>
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSAddToContactViewController
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
|
||||
{
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
}
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
_recipientId = recipientId;
|
||||
}
|
||||
|
||||
#pragma mark - ContactEditingDelegate
|
||||
|
||||
- (void)didFinishEditingContact
|
||||
{
|
||||
OWSLogDebug(@"");
|
||||
[self dismissViewControllerAnimated:NO
|
||||
completion:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - CNContactViewControllerDelegate
|
||||
|
||||
- (void)contactViewController:(CNContactViewController *)viewController
|
||||
didCompleteWithContact:(nullable CNContact *)contact
|
||||
{
|
||||
if (contact) {
|
||||
// Saving normally returns you to the "Show Contact" view
|
||||
// which we're not interested in, so we skip it here. There is
|
||||
// an unfortunate blip of the "Show Contact" view on slower devices.
|
||||
OWSLogDebug(@"completed editing contact.");
|
||||
[self dismissViewControllerAnimated:NO
|
||||
completion:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}];
|
||||
} else {
|
||||
OWSLogDebug(@"canceled editing contact.");
|
||||
[self dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - View Lifecycle
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = NSLocalizedString(@"CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
|
||||
@"Label for 'new contact' button in conversation settings view.");
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (nullable NSString *)displayNameForContact:(Contact *)contact
|
||||
{
|
||||
OWSAssertDebug(contact);
|
||||
|
||||
if (contact.fullName.length > 0) {
|
||||
return contact.fullName;
|
||||
}
|
||||
|
||||
for (NSString *email in contact.emails) {
|
||||
if (email.length > 0) {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
for (NSString *phoneNumber in contact.userTextPhoneNumbers) {
|
||||
if (phoneNumber.length > 0) {
|
||||
return phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
contents.title = NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen");
|
||||
|
||||
__weak OWSAddToContactViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
section.headerTitle = NSLocalizedString(
|
||||
@"EDIT_GROUP_CONTACTS_SECTION_TITLE", @"a title for the contacts section of the 'new/update group' view.");
|
||||
|
||||
for (Contact *contact in self.contactsViewHelper.contactsManager.allContacts) {
|
||||
NSString *_Nullable displayName = [self displayNameForContact:contact];
|
||||
if (displayName.length < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Confirm with nancy if this will work.
|
||||
NSString *cellName = [NSString stringWithFormat:@"contact.%@", NSUUID.UUID.UUIDString];
|
||||
[section addItem:[OWSTableItem disclosureItemWithText:displayName
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, cellName)
|
||||
actionBlock:^{
|
||||
[weakSelf presentContactViewControllerForContact:contact];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:section];
|
||||
|
||||
self.contents = contents;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)presentContactViewControllerForContact:(Contact *)contact
|
||||
{
|
||||
OWSAssertDebug(contact);
|
||||
OWSAssertDebug(self.recipientId);
|
||||
|
||||
if (!self.contactsManager.supportsContactEditing) {
|
||||
OWSFailDebug(@"Contact editing not supported");
|
||||
return;
|
||||
}
|
||||
CNContact *_Nullable cnContact = [self.contactsManager cnContactWithId:contact.cnContactId];
|
||||
if (!cnContact) {
|
||||
OWSFailDebug(@"Could not load system contact.");
|
||||
return;
|
||||
}
|
||||
[self.contactsViewHelper presentContactViewControllerForRecipientId:self.recipientId
|
||||
fromViewController:self
|
||||
editImmediately:YES
|
||||
addToExistingCnContact:cnContact];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
|
||||
func FormatAnalyticsLocation(file: String, function: String) -> NSString {
|
||||
return "\((file as NSString).lastPathComponent):\(function)" as NSString
|
||||
}
|
||||
|
||||
func OWSProdError(_ eventName: String, file: String, function: String, line: Int32) {
|
||||
let location = FormatAnalyticsLocation(file: file, function: function)
|
||||
OWSAnalytics
|
||||
.logEvent(eventName, severity: .error, parameters: nil, location: location.utf8String!, line:line)
|
||||
}
|
||||
|
||||
func OWSProdInfo(_ eventName: String, file: String, function: String, line: Int32) {
|
||||
let location = FormatAnalyticsLocation(file: file, function: function)
|
||||
OWSAnalytics
|
||||
.logEvent(eventName, severity: .info, parameters: nil, location: location.utf8String!, line:line)
|
||||
}
|
|
@ -411,25 +411,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.backupIO = [[OWSBackupIO alloc] initWithJobTempDirPath:self.jobTempDirPath];
|
||||
|
||||
// We need to verify that we have a valid account.
|
||||
// Otherwise, if we re-register on another device, we
|
||||
// continue to backup on our old device, overwriting
|
||||
// backups from the new device.
|
||||
//
|
||||
// We use an arbitrary request that requires authentication
|
||||
// to verify our account state.
|
||||
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
TSRequest *currentSignedPreKey = [OWSRequestFactory currentSignedPreKeyRequest];
|
||||
[[TSNetworkManager sharedManager] makeRequest:currentSignedPreKey
|
||||
success:^(NSURLSessionDataTask *task, NSDictionary *responseObject) {
|
||||
resolve(@(1));
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||
// TODO: We may want to surface this in the UI.
|
||||
OWSLogError(@"could not verify account status: %@.", error);
|
||||
resolve(error);
|
||||
}];
|
||||
}];
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)fetchAllRecords
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#import "OWSBackupSettingsViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "ThreadUtil.h"
|
||||
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
|
|
|
@ -4,22 +4,19 @@
|
|||
|
||||
#import "OWSConversationSettingsViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "FingerprintViewController.h"
|
||||
#import "OWSAddToContactViewController.h"
|
||||
|
||||
|
||||
|
||||
#import "OWSBlockingManager.h"
|
||||
#import "OWSSoundSettingsViewController.h"
|
||||
#import "PhoneNumber.h"
|
||||
#import "ShowGroupMembersViewController.h"
|
||||
|
||||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "UpdateGroupViewController.h"
|
||||
#import <Curve25519Kit/Curve25519.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSAvatarBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/OWSSounds.h>
|
||||
#import <SignalUtilitiesKit/OWSUserProfile.h>
|
||||
|
@ -27,7 +24,7 @@
|
|||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
|
@ -42,8 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
const CGFloat kIconViewLength = 24;
|
||||
|
||||
@interface OWSConversationSettingsViewController () <ContactEditingDelegate,
|
||||
ContactsViewHelperDelegate,
|
||||
@interface OWSConversationSettingsViewController () <
|
||||
#ifdef SHOW_COLOR_PICKER
|
||||
ColorPickerDelegate,
|
||||
#endif
|
||||
|
@ -107,7 +103,6 @@ const CGFloat kIconViewLength = 24;
|
|||
|
||||
- (void)commonInit
|
||||
{
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
|
||||
[self observeNotifications];
|
||||
}
|
||||
|
@ -119,11 +114,6 @@ const CGFloat kIconViewLength = 24;
|
|||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (SSKMessageSenderJobQueue *)messageSenderJobQueue
|
||||
{
|
||||
return SSKEnvironment.shared.messageSenderJobQueue;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
@ -131,16 +121,6 @@ const CGFloat kIconViewLength = 24;
|
|||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
- (OWSContactsManager *)contactsManager
|
||||
{
|
||||
return Environment.shared.contactsManager;
|
||||
}
|
||||
|
||||
- (OWSMessageSender *)messageSender
|
||||
{
|
||||
return SSKEnvironment.shared.messageSender;
|
||||
}
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return [OWSBlockingManager sharedManager];
|
||||
|
@ -174,7 +154,7 @@ const CGFloat kIconViewLength = 24;
|
|||
{
|
||||
NSString *threadName = self.thread.name;
|
||||
if (self.thread.contactIdentifier) {
|
||||
return [self.contactsManager profileNameForRecipientId:self.thread.contactIdentifier];
|
||||
return [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.thread.contactIdentifier avoidingWriteTransaction:YES];
|
||||
} else if (threadName.length == 0 && [self isGroupThread]) {
|
||||
threadName = [MessageStrings newGroupDefaultTitle];
|
||||
}
|
||||
|
@ -186,20 +166,20 @@ const CGFloat kIconViewLength = 24;
|
|||
return [self.thread isKindOfClass:[TSGroupThread class]];
|
||||
}
|
||||
|
||||
- (BOOL)isOpenGroupChat
|
||||
- (BOOL)isOpenGroup
|
||||
{
|
||||
if ([self isGroupThread]) {
|
||||
TSGroupThread *thread = (TSGroupThread *)self.thread;
|
||||
return thread.isPublicChat;
|
||||
return thread.isOpenGroup;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
-(BOOL)isPrivateGroupChat
|
||||
-(BOOL)isClosedGroup
|
||||
{
|
||||
if (self.isGroupThread) {
|
||||
TSGroupThread *thread = (TSGroupThread *)self.thread;
|
||||
return !thread.isRSSFeed && !thread.isPublicChat;
|
||||
return thread.groupModel.groupType == closedGroup;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -209,31 +189,6 @@ const CGFloat kIconViewLength = 24;
|
|||
OWSAssertDebug(thread);
|
||||
self.thread = thread;
|
||||
self.uiDatabaseConnection = uiDatabaseConnection;
|
||||
|
||||
[self updateEditButton];
|
||||
}
|
||||
|
||||
- (void)updateEditButton
|
||||
{
|
||||
OWSAssertDebug(self.thread);
|
||||
|
||||
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing
|
||||
&& self.hasExistingContact) {
|
||||
self.navigationItem.rightBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_TXT", nil)
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(didTapEditButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"edit")];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)hasExistingContact
|
||||
{
|
||||
OWSAssertDebug([self.thread isKindOfClass:[TSContactThread class]]);
|
||||
TSContactThread *contactThread = (TSContactThread *)self.thread;
|
||||
NSString *recipientId = contactThread.contactIdentifier;
|
||||
return [self.contactsManager hasSignalAccountForRecipientId:recipientId];
|
||||
}
|
||||
|
||||
#pragma mark - ContactEditingDelegate
|
||||
|
@ -265,16 +220,6 @@ const CGFloat kIconViewLength = 24;
|
|||
}
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
// Loki: Original code
|
||||
// ========
|
||||
// [self updateTableContents];
|
||||
// ========
|
||||
}
|
||||
|
||||
#pragma mark - View Lifecycle
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
@ -322,8 +267,6 @@ const CGFloat kIconViewLength = 24;
|
|||
self.showVerificationOnAppear = NO;
|
||||
if (self.isGroupThread) {
|
||||
[self showGroupMembersView];
|
||||
} else {
|
||||
[self showVerificationView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,49 +293,6 @@ const CGFloat kIconViewLength = 24;
|
|||
mainSection.customHeaderHeight = isSmallScreen ? @(201.f) : @(208.f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loki: Original code
|
||||
* ========
|
||||
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing
|
||||
&& !self.hasExistingContact) {
|
||||
[mainSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
return [weakSelf
|
||||
disclosureCellWithName:
|
||||
NSLocalizedString(@"CONVERSATION_SETTINGS_NEW_CONTACT",
|
||||
@"Label for 'new contact' button in conversation settings view.")
|
||||
iconName:@"table_ic_new_contact"
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
OWSConversationSettingsViewController, @"new_contact")];
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf presentContactViewController];
|
||||
}]];
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
return [weakSelf
|
||||
disclosureCellWithName:
|
||||
NSLocalizedString(@"CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
|
||||
@"Label for 'new contact' button in conversation settings view.")
|
||||
iconName:@"table_ic_add_to_existing_contact"
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
OWSConversationSettingsViewController,
|
||||
@"add_to_existing_contact")];
|
||||
}
|
||||
actionBlock:^{
|
||||
OWSConversationSettingsViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
TSContactThread *contactThread = (TSContactThread *)strongSelf.thread;
|
||||
NSString *recipientId = contactThread.contactIdentifier;
|
||||
[strongSelf presentAddToContactViewControllerWithRecipientId:recipientId];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (SSKFeatureFlags.conversationSearch) {
|
||||
* ========
|
||||
*/
|
||||
|
||||
if ([self.thread isKindOfClass:TSContactThread.class]) {
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
|
@ -433,79 +333,8 @@ const CGFloat kIconViewLength = 24;
|
|||
actionBlock:^{
|
||||
[weakSelf tappedConversationSearch];
|
||||
}]];
|
||||
/*
|
||||
}
|
||||
|
||||
if (!isNoteToSelf && !self.isGroupThread && self.thread.hasSafetyNumbers) {
|
||||
[mainSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
return [weakSelf
|
||||
disclosureCellWithName:NSLocalizedString(@"VERIFY_PRIVACY",
|
||||
@"Label for button or row which allows users to verify the "
|
||||
@"safety number of another user.")
|
||||
iconName:@"table_ic_not_verified"
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
OWSConversationSettingsViewController, @"safety_numbers")];
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf showVerificationView];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (isNoteToSelf) {
|
||||
// Skip the profile whitelist.
|
||||
} else if ([self.profileManager isThreadInProfileWhitelist:self.thread]) {
|
||||
[mainSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
OWSConversationSettingsViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
return [strongSelf
|
||||
labelCellWithName:
|
||||
(strongSelf.isGroupThread
|
||||
? NSLocalizedString(
|
||||
@"CONVERSATION_SETTINGS_VIEW_PROFILE_IS_SHARED_WITH_GROUP",
|
||||
@"Indicates that user's profile has been shared with a group.")
|
||||
: NSLocalizedString(
|
||||
@"CONVERSATION_SETTINGS_VIEW_PROFILE_IS_SHARED_WITH_USER",
|
||||
@"Indicates that user's profile has been shared with a user."))
|
||||
iconName:@"table_ic_share_profile"
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
OWSConversationSettingsViewController,
|
||||
@"profile_is_shared")];
|
||||
}
|
||||
actionBlock:nil]];
|
||||
} else {
|
||||
[mainSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
OWSConversationSettingsViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
UITableViewCell *cell = [strongSelf
|
||||
disclosureCellWithName:
|
||||
(strongSelf.isGroupThread
|
||||
? NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE_WITH_GROUP",
|
||||
@"Action that shares user profile with a group.")
|
||||
: NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE_WITH_USER",
|
||||
@"Action that shares user profile with a user."))
|
||||
iconName:@"table_ic_share_profile"
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
OWSConversationSettingsViewController, @"share_profile")];
|
||||
cell.userInteractionEnabled = !strongSelf.hasLeftGroup;
|
||||
|
||||
return cell;
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf showShareProfileAlert];
|
||||
}]];
|
||||
}
|
||||
* =======
|
||||
*/
|
||||
|
||||
if (![self isOpenGroupChat]) {
|
||||
if (![self isOpenGroup]) {
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UITableViewCell *cell = [OWSTableItem newCell];
|
||||
|
@ -652,12 +481,10 @@ const CGFloat kIconViewLength = 24;
|
|||
__block BOOL isUserMember = NO;
|
||||
if (self.isGroupThread) {
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
isUserMember = [(TSGroupThread *)self.thread isUserMemberInGroup:userPublicKey transaction:transaction];
|
||||
}];
|
||||
isUserMember = [(TSGroupThread *)self.thread isUserMemberInGroup:userPublicKey];
|
||||
}
|
||||
|
||||
if (self.isGroupThread && self.isPrivateGroupChat && isUserMember) {
|
||||
if (self.isGroupThread && self.isClosedGroup && isUserMember) {
|
||||
if (((TSGroupThread *)self.thread).usesSharedSenderKeys) {
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
|
@ -675,20 +502,6 @@ const CGFloat kIconViewLength = 24;
|
|||
}]
|
||||
];
|
||||
}
|
||||
// [mainSection addItem:[OWSTableItem
|
||||
// itemWithCustomCellBlock:^{
|
||||
// UITableViewCell *cell =
|
||||
// [weakSelf disclosureCellWithName:NSLocalizedString(@"LIST_GROUP_MEMBERS_ACTION",
|
||||
// @"table cell label in conversation settings")
|
||||
// iconName:@"table_ic_group_members"
|
||||
// accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
// OWSConversationSettingsViewController, @"group_members")];
|
||||
// return cell;
|
||||
// }
|
||||
// actionBlock:^{
|
||||
// [weakSelf showGroupMembersView];
|
||||
// }]
|
||||
// ];
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UITableViewCell *cell =
|
||||
|
@ -711,11 +524,6 @@ const CGFloat kIconViewLength = 24;
|
|||
// Mute thread section.
|
||||
|
||||
if (!isNoteToSelf) {
|
||||
// OWSTableSection *notificationsSection = [OWSTableSection new];
|
||||
// We need a section header to separate the notifications UI from the group settings UI.
|
||||
// notificationsSection.headerTitle = NSLocalizedString(
|
||||
// @"SETTINGS_SECTION_NOTIFICATIONS", @"Label for the notifications section of conversation settings view.");
|
||||
|
||||
[mainSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
|
@ -825,9 +633,6 @@ const CGFloat kIconViewLength = 24;
|
|||
actionBlock:^{
|
||||
[weakSelf showMuteUnmuteActionSheet];
|
||||
}]];
|
||||
// mainSection.footerTitle = NSLocalizedString(
|
||||
// @"MUTE_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of muting a thread.");
|
||||
// [contents addSection:notificationsSection];
|
||||
}
|
||||
// Block Conversation section.
|
||||
|
||||
|
@ -885,19 +690,6 @@ const CGFloat kIconViewLength = 24;
|
|||
return 12.f;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)cellWithName:(NSString *)name
|
||||
iconName:(NSString *)iconName
|
||||
disclosureIconColor:(UIColor *)disclosureIconColor
|
||||
{
|
||||
UITableViewCell *cell = [self cellWithName:name iconName:iconName];
|
||||
OWSColorPickerAccessoryView *accessoryView =
|
||||
[[OWSColorPickerAccessoryView alloc] initWithColor:disclosureIconColor];
|
||||
[accessoryView sizeToFit];
|
||||
cell.accessoryView = accessoryView;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)cellWithName:(NSString *)name iconName:(NSString *)iconName
|
||||
{
|
||||
OWSAssertDebug(iconName.length > 0);
|
||||
|
@ -1031,24 +823,6 @@ static CGRect oldframe;
|
|||
return stackView;
|
||||
}
|
||||
|
||||
- (void)conversationNameTouched:(UIGestureRecognizer *)sender
|
||||
{
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
if (self.isGroupThread) {
|
||||
CGPoint location = [sender locationInView:self.avatarView];
|
||||
if (CGRectContainsPoint(self.avatarView.bounds, location)) {
|
||||
[self showUpdateGroupView:UpdateGroupMode_EditGroupAvatar];
|
||||
} else {
|
||||
[self showUpdateGroupView:UpdateGroupMode_EditGroupName];
|
||||
}
|
||||
} else {
|
||||
if (self.contactsManager.supportsContactEditing) {
|
||||
[self presentContactViewController];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIImageView *)viewForIconWithName:(NSString *)iconName
|
||||
{
|
||||
UIImage *icon = [UIImage imageNamed:iconName];
|
||||
|
@ -1102,11 +876,15 @@ static CGRect oldframe;
|
|||
createdInExistingGroup:NO];
|
||||
[infoMessage saveWithTransaction:transaction];
|
||||
|
||||
// TODO TODO TODO
|
||||
|
||||
/*
|
||||
OWSDisappearingMessagesConfigurationMessage *message = [[OWSDisappearingMessagesConfigurationMessage alloc]
|
||||
initWithConfiguration:self.disappearingMessagesConfiguration
|
||||
thread:self.thread];
|
||||
|
||||
[self.messageSenderJobQueue addMessage:message transaction:transaction];
|
||||
*/
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
@ -1122,14 +900,6 @@ static CGRect oldframe;
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)showVerificationView
|
||||
{
|
||||
NSString *recipientId = self.thread.contactIdentifier;
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[FingerprintViewController presentFromViewController:self recipientId:recipientId];
|
||||
}
|
||||
|
||||
- (void)showGroupMembersView
|
||||
{
|
||||
TSGroupThread *thread = (TSGroupThread *)self.thread;
|
||||
|
@ -1137,57 +907,6 @@ static CGRect oldframe;
|
|||
[self.navigationController pushViewController:groupMembersVC animated:YES];
|
||||
}
|
||||
|
||||
- (void)showUpdateGroupView:(UpdateGroupMode)mode
|
||||
{
|
||||
OWSAssertDebug(self.conversationSettingsViewDelegate);
|
||||
|
||||
UpdateGroupViewController *updateGroupViewController = [UpdateGroupViewController new];
|
||||
updateGroupViewController.conversationSettingsViewDelegate = self.conversationSettingsViewDelegate;
|
||||
updateGroupViewController.thread = (TSGroupThread *)self.thread;
|
||||
updateGroupViewController.mode = mode;
|
||||
[self.navigationController pushViewController:updateGroupViewController animated:YES];
|
||||
}
|
||||
|
||||
- (void)presentContactViewController
|
||||
{
|
||||
if (!self.contactsManager.supportsContactEditing) {
|
||||
OWSFailDebug(@"Contact editing not supported");
|
||||
return;
|
||||
}
|
||||
if (![self.thread isKindOfClass:[TSContactThread class]]) {
|
||||
OWSFailDebug(@"unexpected thread: %@", [self.thread class]);
|
||||
return;
|
||||
}
|
||||
|
||||
TSContactThread *contactThread = (TSContactThread *)self.thread;
|
||||
[self.contactsViewHelper presentContactViewControllerForRecipientId:contactThread.contactIdentifier
|
||||
fromViewController:self
|
||||
editImmediately:YES];
|
||||
}
|
||||
|
||||
- (void)presentAddToContactViewControllerWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
if (!self.contactsManager.supportsContactEditing) {
|
||||
// Should not expose UI that lets the user get here.
|
||||
OWSFailDebug(@"Contact editing not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.contactsManager.isSystemContactsAuthorized) {
|
||||
[self.contactsViewHelper presentMissingContactAccessAlertControllerFromViewController:self];
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAddToContactViewController *viewController = [OWSAddToContactViewController new];
|
||||
[viewController configureWithRecipientId:recipientId];
|
||||
[self.navigationController pushViewController:viewController animated:YES];
|
||||
}
|
||||
|
||||
- (void)didTapEditButton
|
||||
{
|
||||
[self presentContactViewController];
|
||||
}
|
||||
|
||||
- (void)editGroup
|
||||
{
|
||||
LKEditClosedGroupVC *editClosedGroupVC = [[LKEditClosedGroupVC alloc] initWithThreadID:self.thread.uniqueId];
|
||||
|
@ -1218,7 +937,7 @@ static CGRect oldframe;
|
|||
{
|
||||
if (self.isGroupThread) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
||||
return !groupThread.isLocalUserInGroup;
|
||||
return !groupThread.isCurrentUserInGroup;
|
||||
}
|
||||
|
||||
return NO;
|
||||
|
@ -1233,14 +952,6 @@ static CGRect oldframe;
|
|||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[[LKClosedGroupsProtocol leaveGroupWithPublicKey:groupPublicKey transaction:transaction] retainUntilComplete];
|
||||
}];
|
||||
} else {
|
||||
TSOutgoingMessage *message =
|
||||
[TSOutgoingMessage outgoingMessageInThread:gThread groupMetaMessage:TSGroupMetaMessageQuit expiresInSeconds:0];
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[self.messageSenderJobQueue addMessage:message transaction:transaction];
|
||||
[gThread leaveGroupWithTransaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
|
@ -1273,8 +984,6 @@ static CGRect oldframe;
|
|||
[BlockListUIUtils showBlockThreadActionSheet:self.thread
|
||||
fromViewController:self
|
||||
blockingManager:self.blockingManager
|
||||
contactsManager:self.contactsManager
|
||||
messageSender:self.messageSender
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
// Update switch state if user cancels action.
|
||||
blockConversationSwitch.on = isBlocked;
|
||||
|
@ -1290,7 +999,6 @@ static CGRect oldframe;
|
|||
[BlockListUIUtils showUnblockThreadActionSheet:self.thread
|
||||
fromViewController:self
|
||||
blockingManager:self.blockingManager
|
||||
contactsManager:self.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
// Update switch state if user cancels action.
|
||||
blockConversationSwitch.on = isBlocked;
|
||||
|
@ -1498,7 +1206,6 @@ static CGRect oldframe;
|
|||
[alert addAction:[UIAlertAction actionWithTitle:@"Reset" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[thread addSessionRestoreDevice:thread.contactIdentifier transaction:transaction];
|
||||
[LKSessionManagementProtocol startSessionResetInThread:thread transaction:transaction];
|
||||
}];
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSDevice.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSDeviceTableViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic) UILabel *nameLabel;
|
||||
@property (nonatomic) UILabel *linkedLabel;
|
||||
@property (nonatomic) UILabel *lastSeenLabel;
|
||||
|
||||
- (void)configureWithDevice:(OWSDevice *)device;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,85 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSDeviceTableViewCell.h"
|
||||
#import "DateUtil.h"
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSDeviceTableViewCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier
|
||||
{
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
[self configure];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configure
|
||||
{
|
||||
self.preservesSuperviewLayoutMargins = YES;
|
||||
self.contentView.preservesSuperviewLayoutMargins = YES;
|
||||
|
||||
self.nameLabel = [UILabel new];
|
||||
self.linkedLabel = [UILabel new];
|
||||
self.lastSeenLabel = [UILabel new];
|
||||
|
||||
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.nameLabel,
|
||||
self.linkedLabel,
|
||||
self.lastSeenLabel,
|
||||
]];
|
||||
stackView.axis = UILayoutConstraintAxisVertical;
|
||||
stackView.alignment = UIStackViewAlignmentLeading;
|
||||
stackView.spacing = 2;
|
||||
[self.contentView addSubview:stackView];
|
||||
[stackView ows_autoPinToSuperviewMargins];
|
||||
}
|
||||
|
||||
- (void)configureWithDevice:(OWSDevice *)device
|
||||
{
|
||||
OWSAssertDebug(device);
|
||||
|
||||
[OWSTableItem configureCell:self];
|
||||
|
||||
self.nameLabel.font = UIFont.ows_dynamicTypeBodyFont;
|
||||
self.linkedLabel.font = UIFont.ows_dynamicTypeCaption1Font;
|
||||
self.lastSeenLabel.font = UIFont.ows_dynamicTypeCaption1Font;
|
||||
|
||||
self.nameLabel.textColor = Theme.primaryColor;
|
||||
self.linkedLabel.textColor = Theme.secondaryColor;
|
||||
self.lastSeenLabel.textColor = Theme.secondaryColor;
|
||||
|
||||
self.nameLabel.text = device.displayName;
|
||||
|
||||
NSString *linkedFormatString
|
||||
= NSLocalizedString(@"DEVICE_LINKED_AT_LABEL", @"{{Short Date}} when device was linked.");
|
||||
self.linkedLabel.text =
|
||||
[NSString stringWithFormat:linkedFormatString, [DateUtil.dateFormatter stringFromDate:device.createdAt]];
|
||||
|
||||
NSString *lastSeenFormatString = NSLocalizedString(
|
||||
@"DEVICE_LAST_ACTIVE_AT_LABEL", @"{{Short Date}} when device last communicated with Signal Server.");
|
||||
|
||||
NSDate *displayedLastSeenAt;
|
||||
// lastSeenAt is stored at day granularity. At midnight UTC.
|
||||
// Making it likely that when you first link a device it will
|
||||
// be "last seen" the day before it was created, which looks broken.
|
||||
if ([device.lastSeenAt compare:device.createdAt] == NSOrderedDescending) {
|
||||
displayedLastSeenAt = device.lastSeenAt;
|
||||
} else {
|
||||
displayedLastSeenAt = device.createdAt;
|
||||
}
|
||||
|
||||
self.lastSeenLabel.text =
|
||||
[NSString stringWithFormat:lastSeenFormatString, [DateUtil.dateFormatter stringFromDate:displayedLastSeenAt]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -9,7 +9,7 @@
|
|||
#import <SignalUtilitiesKit/OWSUserProfile.h>
|
||||
#import <SignalUtilitiesKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
#import <SignalUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^SubmitDebugLogsCompletion)(void);
|
||||
|
||||
@interface Pastelog : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
+ (void)submitLogs;
|
||||
+ (void)submitLogsWithCompletion:(nullable SubmitDebugLogsCompletion)completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,647 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "ThreadUtil.h"
|
||||
#import "zlib.h"
|
||||
#import <AFNetworking/AFNetworking.h>
|
||||
#import <SSZipArchive/SSZipArchive.h>
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/DebugLogger.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/AppContext.h>
|
||||
#import <SignalUtilitiesKit/MimeTypeUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSContactThread.h>
|
||||
#import <sys/sysctl.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^UploadDebugLogsSuccess)(NSURL *url);
|
||||
typedef void (^UploadDebugLogsFailure)(NSString *localizedErrorMessage);
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@class DebugLogUploader;
|
||||
|
||||
typedef void (^DebugLogUploadSuccess)(DebugLogUploader *uploader, NSURL *url);
|
||||
typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error);
|
||||
|
||||
@interface DebugLogUploader : NSObject
|
||||
|
||||
@property (nonatomic) NSURL *fileUrl;
|
||||
@property (nonatomic) NSString *mimeType;
|
||||
@property (nonatomic, nullable) DebugLogUploadSuccess success;
|
||||
@property (nonatomic, nullable) DebugLogUploadFailure failure;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation DebugLogUploader
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
}
|
||||
|
||||
- (void)uploadFileWithURL:(NSURL *)fileUrl
|
||||
mimeType:(NSString *)mimeType
|
||||
success:(DebugLogUploadSuccess)success
|
||||
failure:(DebugLogUploadFailure)failure
|
||||
{
|
||||
OWSAssertDebug(fileUrl);
|
||||
OWSAssertDebug(mimeType.length > 0);
|
||||
OWSAssertDebug(success);
|
||||
OWSAssertDebug(failure);
|
||||
|
||||
self.fileUrl = fileUrl;
|
||||
self.mimeType = mimeType;
|
||||
self.success = success;
|
||||
self.failure = failure;
|
||||
|
||||
[self getUploadParameters];
|
||||
}
|
||||
|
||||
- (void)getUploadParameters
|
||||
{
|
||||
__weak DebugLogUploader *weakSelf = self;
|
||||
|
||||
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
AFHTTPSessionManager *sessionManager =
|
||||
[[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:sessionConf];
|
||||
sessionManager.requestSerializer = [AFHTTPRequestSerializer serializer];
|
||||
sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
||||
NSString *urlString = @"https://debuglogs.org/";
|
||||
[sessionManager GET:urlString
|
||||
parameters:nil
|
||||
headers:nil
|
||||
progress:nil
|
||||
success:^(NSURLSessionDataTask *task, id _Nullable responseObject) {
|
||||
DebugLogUploader *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![responseObject isKindOfClass:[NSDictionary class]]) {
|
||||
OWSLogError(@"Invalid response: %@, %@", urlString, responseObject);
|
||||
[strongSelf
|
||||
failWithError:OWSErrorWithCodeDescription(OWSErrorCodeDebugLogUploadFailed, @"Invalid response")];
|
||||
return;
|
||||
}
|
||||
NSString *uploadUrl = responseObject[@"url"];
|
||||
if (![uploadUrl isKindOfClass:[NSString class]] || uploadUrl.length < 1) {
|
||||
OWSLogError(@"Invalid response: %@, %@", urlString, responseObject);
|
||||
[strongSelf
|
||||
failWithError:OWSErrorWithCodeDescription(OWSErrorCodeDebugLogUploadFailed, @"Invalid response")];
|
||||
return;
|
||||
}
|
||||
NSDictionary *fields = responseObject[@"fields"];
|
||||
if (![fields isKindOfClass:[NSDictionary class]] || fields.count < 1) {
|
||||
OWSLogError(@"Invalid response: %@, %@", urlString, responseObject);
|
||||
[strongSelf
|
||||
failWithError:OWSErrorWithCodeDescription(OWSErrorCodeDebugLogUploadFailed, @"Invalid response")];
|
||||
return;
|
||||
}
|
||||
for (NSString *fieldName in fields) {
|
||||
NSString *fieldValue = fields[fieldName];
|
||||
if (![fieldName isKindOfClass:[NSString class]] || fieldName.length < 1
|
||||
|| ![fieldValue isKindOfClass:[NSString class]] || fieldValue.length < 1) {
|
||||
OWSLogError(@"Invalid response: %@, %@", urlString, responseObject);
|
||||
[strongSelf failWithError:OWSErrorWithCodeDescription(
|
||||
OWSErrorCodeDebugLogUploadFailed, @"Invalid response")];
|
||||
return;
|
||||
}
|
||||
}
|
||||
NSString *_Nullable uploadKey = fields[@"key"];
|
||||
if (![uploadKey isKindOfClass:[NSString class]] || uploadKey.length < 1) {
|
||||
OWSLogError(@"Invalid response: %@, %@", urlString, responseObject);
|
||||
[strongSelf
|
||||
failWithError:OWSErrorWithCodeDescription(OWSErrorCodeDebugLogUploadFailed, @"Invalid response")];
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a file extension to the upload's key.
|
||||
NSString *fileExtension = strongSelf.fileUrl.lastPathComponent.pathExtension;
|
||||
if (fileExtension.length < 1) {
|
||||
OWSLogError(@"Invalid file url: %@, %@", urlString, responseObject);
|
||||
[strongSelf
|
||||
failWithError:OWSErrorWithCodeDescription(OWSErrorCodeDebugLogUploadFailed, @"Invalid file url")];
|
||||
return;
|
||||
}
|
||||
uploadKey = [uploadKey stringByAppendingPathExtension:fileExtension];
|
||||
NSMutableDictionary *updatedFields = [fields mutableCopy];
|
||||
updatedFields[@"key"] = uploadKey;
|
||||
|
||||
[strongSelf uploadFileWithUploadUrl:uploadUrl fields:updatedFields uploadKey:uploadKey];
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *_Nullable task, NSError *error) {
|
||||
OWSLogError(@"failed: %@", urlString);
|
||||
[weakSelf failWithError:error];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)uploadFileWithUploadUrl:(NSString *)uploadUrl fields:(NSDictionary *)fields uploadKey:(NSString *)uploadKey
|
||||
{
|
||||
OWSAssertDebug(uploadUrl.length > 0);
|
||||
OWSAssertDebug(fields);
|
||||
OWSAssertDebug(uploadKey.length > 0);
|
||||
|
||||
__weak DebugLogUploader *weakSelf = self;
|
||||
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
AFHTTPSessionManager *sessionManager =
|
||||
[[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:sessionConf];
|
||||
sessionManager.requestSerializer = [AFHTTPRequestSerializer serializer];
|
||||
sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||
[sessionManager POST:uploadUrl
|
||||
parameters:@{}
|
||||
headers:nil
|
||||
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
|
||||
for (NSString *fieldName in fields) {
|
||||
NSString *fieldValue = fields[fieldName];
|
||||
[formData appendPartWithFormData:[fieldValue dataUsingEncoding:NSUTF8StringEncoding] name:fieldName];
|
||||
}
|
||||
[formData appendPartWithFormData:[weakSelf.mimeType dataUsingEncoding:NSUTF8StringEncoding]
|
||||
name:@"content-type"];
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [formData appendPartWithFileURL:weakSelf.fileUrl
|
||||
name:@"file"
|
||||
fileName:weakSelf.fileUrl.lastPathComponent
|
||||
mimeType:weakSelf.mimeType
|
||||
error:&error];
|
||||
if (!success || error) {
|
||||
OWSLogError(@"failed: %@, error: %@", uploadUrl, error);
|
||||
}
|
||||
}
|
||||
progress:nil
|
||||
success:^(NSURLSessionDataTask *task, id _Nullable responseObject) {
|
||||
OWSLogVerbose(@"Response: %@, %@", uploadUrl, responseObject);
|
||||
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://debuglogs.org/%@", uploadKey];
|
||||
[self succeedWithUrl:[NSURL URLWithString:urlString]];
|
||||
}
|
||||
failure:^(NSURLSessionDataTask *_Nullable task, NSError *error) {
|
||||
OWSLogError(@"upload: %@ failed with error: %@", uploadUrl, error);
|
||||
[weakSelf failWithError:error];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
|
||||
{
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
|
||||
NSInteger statusCode = httpResponse.statusCode;
|
||||
// We'll accept any 2xx status code.
|
||||
NSInteger statusCodeClass = statusCode - (statusCode % 100);
|
||||
if (statusCodeClass != 200) {
|
||||
OWSLogError(@"statusCode: %zd, %zd", statusCode, statusCodeClass);
|
||||
OWSLogError(@"headers: %@", httpResponse.allHeaderFields);
|
||||
[self failWithError:[NSError errorWithDomain:@"PastelogKit"
|
||||
code:10001
|
||||
userInfo:@{ NSLocalizedDescriptionKey : @"Invalid response code." }]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
[self failWithError:error];
|
||||
}
|
||||
|
||||
- (void)failWithError:(NSError *)error
|
||||
{
|
||||
OWSAssertDebug(error);
|
||||
|
||||
OWSLogError(@"%@", error);
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
// Call the completions exactly once.
|
||||
if (self.failure) {
|
||||
self.failure(self, error);
|
||||
}
|
||||
self.success = nil;
|
||||
self.failure = nil;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)succeedWithUrl:(NSURL *)url
|
||||
{
|
||||
OWSAssertDebug(url);
|
||||
|
||||
OWSLogVerbose(@"%@", url);
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
// Call the completions exactly once.
|
||||
if (self.success) {
|
||||
self.success(self, url);
|
||||
}
|
||||
self.success = nil;
|
||||
self.failure = nil;
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface Pastelog () <UIAlertViewDelegate>
|
||||
|
||||
@property (nonatomic) UIAlertController *loadingAlert;
|
||||
|
||||
@property (nonatomic) DebugLogUploader *currentUploader;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation Pastelog
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static Pastelog *sharedMyManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedMyManager = [[self alloc] initDefault];
|
||||
});
|
||||
return sharedMyManager;
|
||||
}
|
||||
|
||||
- (instancetype)initDefault
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (YapDatabaseConnection *)dbConnection
|
||||
{
|
||||
return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (void)submitLogs
|
||||
{
|
||||
[self submitLogsWithCompletion:nil];
|
||||
}
|
||||
|
||||
+ (void)submitLogsWithCompletion:(nullable SubmitDebugLogsCompletion)completionParam
|
||||
{
|
||||
SubmitDebugLogsCompletion completion = ^{
|
||||
if (completionParam) {
|
||||
// Wait a moment. If PasteLog opens a URL, it needs a moment to complete.
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), completionParam);
|
||||
}
|
||||
};
|
||||
|
||||
[[self sharedManager] uploadLogsWithUIWithSuccess:^(NSURL *url) {
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_TITLE", @"Title of the debug log alert.")
|
||||
message:NSLocalizedString(@"DEBUG_LOG_ALERT_MESSAGE", @"Message of the debug log alert.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert
|
||||
addAction:[UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_EMAIL",
|
||||
@"Label for the 'email debug log' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_email")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[Pastelog.sharedManager submitEmail:url];
|
||||
|
||||
completion();
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_COPY_LINK",
|
||||
@"Label for the 'copy link' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"copy_link")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
||||
[pb setString:url.absoluteString];
|
||||
|
||||
completion();
|
||||
}]];
|
||||
#ifdef DEBUG
|
||||
[alert
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_SEND_TO_SELF",
|
||||
@"Label for the 'send to self' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_to_self")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[Pastelog.sharedManager sendToSelf:url];
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:
|
||||
NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_SEND_TO_LAST_THREAD",
|
||||
@"Label for the 'send to last thread' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_to_last_thread")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[Pastelog.sharedManager sendToMostRecentThread:url];
|
||||
}]];
|
||||
#endif
|
||||
[alert
|
||||
addAction:
|
||||
[UIAlertAction actionWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_BUG_REPORT",
|
||||
@"Label for the 'Open a Bug Report' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"submit_bug_report")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[Pastelog.sharedManager prepareRedirection:url completion:completion];
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_OPTION_SHARE",
|
||||
@"Label for the 'Share' option of the debug log alert.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"share")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[AttachmentSharing showShareUIForText:url.absoluteString
|
||||
completion:completion];
|
||||
}]];
|
||||
[alert addAction:[OWSAlerts cancelAction]];
|
||||
UIViewController *presentingViewController
|
||||
= UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts;
|
||||
[presentingViewController presentAlert:alert animated:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)uploadLogsWithUIWithSuccess:(UploadDebugLogsSuccess)successParam {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[ModalActivityIndicatorViewController
|
||||
presentFromViewController:UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts
|
||||
canCancel:YES
|
||||
backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) {
|
||||
[self
|
||||
uploadLogsWithSuccess:^(NSURL *url) {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
if (modalActivityIndicator.wasCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[modalActivityIndicator dismissWithCompletion:^{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
successParam(url);
|
||||
}];
|
||||
}
|
||||
failure:^(NSString *localizedErrorMessage) {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
if (modalActivityIndicator.wasCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[modalActivityIndicator dismissWithCompletion:^{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[Pastelog showFailureAlertWithMessage:localizedErrorMessage];
|
||||
}];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)uploadLogsWithSuccess:(UploadDebugLogsSuccess)successParam failure:(UploadDebugLogsFailure)failureParam {
|
||||
OWSAssertDebug(successParam);
|
||||
OWSAssertDebug(failureParam);
|
||||
|
||||
// Ensure that we call the completions on the main thread.
|
||||
UploadDebugLogsSuccess success = ^(NSURL *url) {
|
||||
DispatchMainThreadSafe(^{
|
||||
successParam(url);
|
||||
});
|
||||
};
|
||||
UploadDebugLogsFailure failure = ^(NSString *localizedErrorMessage) {
|
||||
DispatchMainThreadSafe(^{
|
||||
failureParam(localizedErrorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
// Phase 1. Make a local copy of all of the log files.
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setLocale:[NSLocale currentLocale]];
|
||||
[dateFormatter setDateFormat:@"yyyy.MM.dd hh.mm.ss"];
|
||||
NSString *dateString = [dateFormatter stringFromDate:[NSDate new]];
|
||||
NSString *logsName = [[dateString stringByAppendingString:@" "] stringByAppendingString:NSUUID.UUID.UUIDString];
|
||||
NSString *tempDirectory = OWSTemporaryDirectory();
|
||||
NSString *zipFilePath =
|
||||
[tempDirectory stringByAppendingPathComponent:[logsName stringByAppendingPathExtension:@"zip"]];
|
||||
NSString *zipDirPath = [tempDirectory stringByAppendingPathComponent:logsName];
|
||||
[OWSFileSystem ensureDirectoryExists:zipDirPath];
|
||||
|
||||
NSArray<NSString *> *logFilePaths = DebugLogger.sharedLogger.allLogFilePaths;
|
||||
if (logFilePaths.count < 1) {
|
||||
failure(NSLocalizedString(@"DEBUG_LOG_ALERT_NO_LOGS", @"Error indicating that no debug logs could be found."));
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *logFilePath in logFilePaths) {
|
||||
NSString *copyFilePath = [zipDirPath stringByAppendingPathComponent:logFilePath.lastPathComponent];
|
||||
NSError *error;
|
||||
[[NSFileManager defaultManager] copyItemAtPath:logFilePath toPath:copyFilePath error:&error];
|
||||
if (error) {
|
||||
failure(NSLocalizedString(
|
||||
@"DEBUG_LOG_ALERT_COULD_NOT_COPY_LOGS", @"Error indicating that the debug logs could not be copied."));
|
||||
return;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:copyFilePath];
|
||||
}
|
||||
|
||||
// Phase 2. Zip up the log files.
|
||||
BOOL zipSuccess = [SSZipArchive createZipFileAtPath:zipFilePath
|
||||
withContentsOfDirectory:zipDirPath
|
||||
keepParentDirectory:YES
|
||||
compressionLevel:Z_DEFAULT_COMPRESSION
|
||||
password:nil
|
||||
AES:NO
|
||||
progressHandler:nil];
|
||||
if (!zipSuccess) {
|
||||
failure(NSLocalizedString(
|
||||
@"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS", @"Error indicating that the debug logs could not be packaged."));
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSFileSystem protectFileOrFolderAtPath:zipFilePath];
|
||||
[OWSFileSystem deleteFile:zipDirPath];
|
||||
|
||||
// Phase 3. Upload the log files.
|
||||
|
||||
__weak Pastelog *weakSelf = self;
|
||||
self.currentUploader = [DebugLogUploader new];
|
||||
[self.currentUploader uploadFileWithURL:[NSURL fileURLWithPath:zipFilePath]
|
||||
mimeType:OWSMimeTypeApplicationZip
|
||||
success:^(DebugLogUploader *uploader, NSURL *url) {
|
||||
if (uploader != weakSelf.currentUploader) {
|
||||
// Ignore events from obsolete uploaders.
|
||||
return;
|
||||
}
|
||||
[OWSFileSystem deleteFile:zipFilePath];
|
||||
success(url);
|
||||
}
|
||||
failure:^(DebugLogUploader *uploader, NSError *error) {
|
||||
if (uploader != weakSelf.currentUploader) {
|
||||
// Ignore events from obsolete uploaders.
|
||||
return;
|
||||
}
|
||||
[OWSFileSystem deleteFile:zipFilePath];
|
||||
failure(NSLocalizedString(
|
||||
@"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG", @"Error indicating that a debug log could not be uploaded."));
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)showFailureAlertWithMessage:(NSString *)message
|
||||
{
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:NSLocalizedString(@"DEBUG_LOG_ALERT_TITLE",
|
||||
@"Title of the alert shown for failures while uploading debug logs.")
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:nil]];
|
||||
UIViewController *presentingViewController = UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts;
|
||||
[presentingViewController presentAlert:alert animated:NO];
|
||||
}
|
||||
|
||||
#pragma mark Logs submission
|
||||
|
||||
- (void)submitEmail:(NSURL *)url
|
||||
{
|
||||
NSString *emailAddress = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"LOGS_EMAIL"];
|
||||
|
||||
NSMutableString *body = [NSMutableString new];
|
||||
|
||||
[body appendFormat:@"Tell us about the issue: \n\n\n"];
|
||||
|
||||
size_t size;
|
||||
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
|
||||
char *machine = malloc(size);
|
||||
sysctlbyname("hw.machine", machine, &size, NULL, 0);
|
||||
NSString *platform = [NSString stringWithUTF8String:machine];
|
||||
free(machine);
|
||||
|
||||
[body appendFormat:@"Device: %@ (%@)\n", UIDevice.currentDevice.model, platform];
|
||||
[body appendFormat:@"iOS Version: %@ \n", [UIDevice currentDevice].systemVersion];
|
||||
[body appendFormat:@"Signal Version: %@ \n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
|
||||
[body appendFormat:@"Log URL: %@ \n", url];
|
||||
|
||||
NSString *escapedBody =
|
||||
[body stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet];
|
||||
NSString *urlString =
|
||||
[NSString stringWithFormat:@"mailto:%@?subject=iOS%%20Debug%%20Log&body=%@", emailAddress, escapedBody];
|
||||
|
||||
BOOL success = [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlString]];
|
||||
if (!success) {
|
||||
OWSLogError(@"Could not open Email app.");
|
||||
[OWSAlerts showErrorAlertWithMessage:NSLocalizedString(@"DEBUG_LOG_COULD_NOT_EMAIL",
|
||||
@"Error indicating that the app could not launch the Email app.")];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prepareRedirection:(NSURL *)url completion:(SubmitDebugLogsCompletion)completion
|
||||
{
|
||||
OWSAssertDebug(completion);
|
||||
|
||||
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
||||
[pb setString:url.absoluteString];
|
||||
|
||||
UIAlertController *alert =
|
||||
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"DEBUG_LOG_GITHUB_ISSUE_ALERT_TITLE",
|
||||
@"Title of the alert before redirecting to GitHub Issues.")
|
||||
message:NSLocalizedString(@"DEBUG_LOG_GITHUB_ISSUE_ALERT_MESSAGE",
|
||||
@"Message of the alert before redirecting to GitHub Issues.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert
|
||||
addAction:[UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"OK", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[UIApplication.sharedApplication
|
||||
openURL:[NSURL
|
||||
URLWithString:[[NSBundle mainBundle]
|
||||
objectForInfoDictionaryKey:@"LOGS_URL"]]];
|
||||
|
||||
completion();
|
||||
}]];
|
||||
UIViewController *presentingViewController = UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts;
|
||||
[presentingViewController presentAlert:alert animated:NO];
|
||||
}
|
||||
|
||||
- (void)sendToSelf:(NSURL *)url
|
||||
{
|
||||
if (![self.tsAccountManager isRegistered]) {
|
||||
return;
|
||||
}
|
||||
NSString *recipientId = [TSAccountManager localNumber];
|
||||
|
||||
DispatchMainThreadSafe(^{
|
||||
__block TSThread *thread = nil;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction];
|
||||
}];
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[ThreadUtil enqueueMessageWithText:url.absoluteString
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
});
|
||||
|
||||
// Also copy to pasteboard.
|
||||
[[UIPasteboard generalPasteboard] setString:url.absoluteString];
|
||||
}
|
||||
|
||||
- (void)sendToMostRecentThread:(NSURL *)url
|
||||
{
|
||||
if (![self.tsAccountManager isRegistered]) {
|
||||
return;
|
||||
}
|
||||
|
||||
__block TSThread *thread = nil;
|
||||
[OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
thread = [[transaction ext:TSThreadDatabaseViewExtensionName] firstObjectInGroup:TSInboxGroup];
|
||||
}];
|
||||
DispatchMainThreadSafe(^{
|
||||
if (thread) {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[ThreadUtil enqueueMessageWithText:url.absoluteString
|
||||
inThread:thread
|
||||
quotedReplyModel:nil
|
||||
linkPreviewDraft:nil
|
||||
transaction:transaction];
|
||||
}];
|
||||
} else {
|
||||
[Pastelog showFailureAlertWithMessage:@"Could not find last thread."];
|
||||
}
|
||||
});
|
||||
|
||||
// Also copy to pasteboard.
|
||||
[[UIPasteboard generalPasteboard] setString:url.absoluteString];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class PinEntryView;
|
||||
|
||||
@protocol PinEntryViewDelegate <NSObject>
|
||||
|
||||
- (void)pinEntryView:(PinEntryView *)entryView submittedPinCode:(NSString *)pinCode;
|
||||
- (void)pinEntryViewForgotPinLinkTapped:(PinEntryView *)entryView;
|
||||
|
||||
@optional
|
||||
- (void)pinEntryView:(PinEntryView *)entryView pinCodeDidChange:(NSString *)pinCode;
|
||||
|
||||
@end
|
||||
|
||||
@interface PinEntryView : UIView
|
||||
|
||||
@property (nonatomic, weak, nullable) id<PinEntryViewDelegate> delegate;
|
||||
@property (nonatomic, readonly) BOOL hasValidPin;
|
||||
@property (nullable, nonatomic) NSString *instructionsText;
|
||||
@property (nullable, nonatomic) NSAttributedString *attributedInstructionsText;
|
||||
@property (nonatomic, readonly) UIFont *boldLabelFont;
|
||||
|
||||
- (void)clearText;
|
||||
- (BOOL)makePinTextFieldFirstResponder;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,227 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "PinEntryView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/ViewControllerUtils.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PinEntryView () <UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic) UITextField *pinTextfield;
|
||||
@property (nonatomic) OWSFlatButton *submitButton;
|
||||
@property (nonatomic) UILabel *instructionsLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation PinEntryView : UIView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self createContents];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - view creation
|
||||
- (UIFont *)labelFont
|
||||
{
|
||||
return [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
}
|
||||
|
||||
- (UIFont *)boldLabelFont
|
||||
{
|
||||
return [UIFont ows_boldFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
}
|
||||
|
||||
- (UILabel *)createLabelWithText:(nullable NSString *)text
|
||||
{
|
||||
UILabel *label = [UILabel new];
|
||||
label.textColor = [Theme primaryColor];
|
||||
label.text = text;
|
||||
label.font = self.labelFont;
|
||||
label.numberOfLines = 0;
|
||||
label.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:label];
|
||||
return label;
|
||||
}
|
||||
|
||||
- (void)createPinTextfield
|
||||
{
|
||||
if (UIDevice.currentDevice.isShorterThanIPhone5) {
|
||||
self.pinTextfield = [DismissableTextField new];
|
||||
} else {
|
||||
self.pinTextfield = [OWSTextField new];
|
||||
}
|
||||
|
||||
self.pinTextfield.textColor = [Theme primaryColor];
|
||||
self.pinTextfield.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(30.f, 36.f)];
|
||||
self.pinTextfield.textAlignment = NSTextAlignmentCenter;
|
||||
self.pinTextfield.keyboardType = UIKeyboardTypeNumberPad;
|
||||
self.pinTextfield.delegate = self;
|
||||
self.pinTextfield.secureTextEntry = YES;
|
||||
self.pinTextfield.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:self.pinTextfield];
|
||||
}
|
||||
|
||||
- (UILabel *)createForgotLink
|
||||
{
|
||||
UILabel *label = [UILabel new];
|
||||
label.textColor = [UIColor ows_materialBlueColor];
|
||||
NSString *text = NSLocalizedString(
|
||||
@"REGISTER_2FA_FORGOT_PIN", @"Label for 'I forgot my PIN' link in the 2FA registration view.");
|
||||
label.attributedText = [[NSAttributedString alloc]
|
||||
initWithString:text
|
||||
attributes:@{
|
||||
NSForegroundColorAttributeName : [UIColor ows_materialBlueColor],
|
||||
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid)
|
||||
}];
|
||||
label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
label.numberOfLines = 0;
|
||||
label.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.userInteractionEnabled = YES;
|
||||
[label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(forgotPinLinkTapped:)]];
|
||||
[self addSubview:label];
|
||||
return label;
|
||||
}
|
||||
|
||||
- (void)createSubmitButton
|
||||
{
|
||||
const CGFloat kSubmitButtonHeight = 47.f;
|
||||
// NOTE: We use ows_signalBrandBlueColor instead of ows_materialBlueColor
|
||||
// throughout the onboarding flow to be consistent with the headers.
|
||||
OWSFlatButton *submitButton =
|
||||
[OWSFlatButton buttonWithTitle:NSLocalizedString(@"REGISTER_2FA_SUBMIT_BUTTON",
|
||||
@"Label for 'submit' button in the 2FA registration view.")
|
||||
font:[OWSFlatButton fontForHeight:kSubmitButtonHeight]
|
||||
titleColor:[UIColor whiteColor]
|
||||
backgroundColor:[UIColor ows_signalBrandBlueColor]
|
||||
target:self
|
||||
selector:@selector(submitButtonWasPressed)];
|
||||
self.submitButton = submitButton;
|
||||
[self addSubview:submitButton];
|
||||
[self.submitButton autoSetDimension:ALDimensionHeight toSize:kSubmitButtonHeight];
|
||||
}
|
||||
|
||||
- (nullable NSString *)instructionsText
|
||||
{
|
||||
return self.instructionsLabel.text;
|
||||
}
|
||||
|
||||
- (void)setInstructionsText:(nullable NSString *)instructionsText
|
||||
{
|
||||
self.instructionsLabel.text = instructionsText;
|
||||
}
|
||||
|
||||
- (nullable NSAttributedString *)attributedInstructionsText
|
||||
{
|
||||
return self.instructionsLabel.attributedText;
|
||||
}
|
||||
|
||||
- (void)setAttributedInstructionsText:(nullable NSAttributedString *)attributedInstructionsText
|
||||
{
|
||||
self.instructionsLabel.attributedText = attributedInstructionsText;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
const CGFloat kVSpacing = ScaleFromIPhone5To7Plus(12, 30);
|
||||
|
||||
UILabel *instructionsLabel = [self createLabelWithText:nil];
|
||||
self.instructionsLabel = instructionsLabel;
|
||||
[instructionsLabel autoPinTopToSuperviewMarginWithInset:kVSpacing];
|
||||
[instructionsLabel autoPinWidthToSuperview];
|
||||
|
||||
UILabel *createForgotLink = [self createForgotLink];
|
||||
[createForgotLink autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:instructionsLabel withOffset:5];
|
||||
[createForgotLink autoPinWidthToSuperview];
|
||||
|
||||
[self createPinTextfield];
|
||||
[self.pinTextfield autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:createForgotLink withOffset:kVSpacing];
|
||||
[self.pinTextfield autoPinWidthToSuperview];
|
||||
|
||||
UIView *underscoreView = [UIView new];
|
||||
underscoreView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.f];
|
||||
[self addSubview:underscoreView];
|
||||
[underscoreView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.pinTextfield withOffset:3];
|
||||
[underscoreView autoPinWidthToSuperview];
|
||||
[underscoreView autoSetDimension:ALDimensionHeight toSize:1.f];
|
||||
|
||||
[self createSubmitButton];
|
||||
[self.submitButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:underscoreView withOffset:kVSpacing];
|
||||
[self.submitButton autoPinWidthToSuperview];
|
||||
[self updateIsSubmitEnabled];
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textField:(UITextField *)textField
|
||||
shouldChangeCharactersInRange:(NSRange)range
|
||||
replacementString:(NSString *)insertionText
|
||||
{
|
||||
|
||||
[ViewControllerUtils ows2FAPINTextField:textField
|
||||
shouldChangeCharactersInRange:range
|
||||
replacementString:insertionText];
|
||||
|
||||
[self updateIsSubmitEnabled];
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(pinEntryView:pinCodeDidChange:)]) {
|
||||
[self.delegate pinEntryView:self pinCodeDidChange:textField.text];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)updateIsSubmitEnabled;
|
||||
{
|
||||
[self.submitButton setEnabled:self.hasValidPin];
|
||||
}
|
||||
|
||||
- (BOOL)makePinTextFieldFirstResponder
|
||||
{
|
||||
return [self.pinTextfield becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (BOOL)hasValidPin
|
||||
{
|
||||
return self.pinTextfield.text.length >= kMin2FAPinLength;
|
||||
}
|
||||
|
||||
- (void)clearText
|
||||
{
|
||||
self.pinTextfield.text = @"";
|
||||
[self updateIsSubmitEnabled];
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)submitButtonWasPressed
|
||||
{
|
||||
[self.delegate pinEntryView:self submittedPinCode:self.pinTextfield.text];
|
||||
}
|
||||
|
||||
- (void)forgotPinLinkTapped:(UIGestureRecognizer *)sender
|
||||
{
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.delegate pinEntryViewForgotPinLinkTapped:self];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,16 +3,16 @@
|
|||
//
|
||||
|
||||
#import "PrivacySettingsTableViewController.h"
|
||||
#import "BlockListViewController.h"
|
||||
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWS2FAManager.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSReadReceiptManager.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
|
@ -83,22 +83,7 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
|
|||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak PrivacySettingsTableViewController *weakSelf = self;
|
||||
|
||||
// Loki: Original code
|
||||
// ========
|
||||
// OWSTableSection *blocklistSection = [OWSTableSection new];
|
||||
// blocklistSection.headerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE", @"Label for the block list section of the settings view");
|
||||
// [blocklistSection
|
||||
// addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE",
|
||||
// @"Label for the block list section of the settings view")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"blocklist"]
|
||||
// actionBlock:^{
|
||||
// [weakSelf showBlocklist];
|
||||
// }]];
|
||||
// [contents addSection:blocklistSection];
|
||||
//
|
||||
// ========
|
||||
|
||||
OWSTableSection *readReceiptsSection = [OWSTableSection new];
|
||||
readReceiptsSection.headerTitle
|
||||
= NSLocalizedString(@"SETTINGS_READ_RECEIPT", @"Label for the 'read receipts' setting.");
|
||||
|
@ -189,104 +174,6 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
|
|||
selector:@selector(didToggleScreenSecuritySwitch:)]];
|
||||
[contents addSection:screenSecuritySection];
|
||||
|
||||
// Loki: Original code
|
||||
// ========
|
||||
// // Allow calls to connect directly vs. using TURN exclusively
|
||||
// OWSTableSection *callingSection = [OWSTableSection new];
|
||||
// callingSection.headerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_SECTION_TITLE_CALLING", @"settings topic header for table section");
|
||||
// callingSection.footerTitle = NSLocalizedString(@"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE_DETAIL",
|
||||
// @"User settings section footer, a detailed explanation");
|
||||
// [callingSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(
|
||||
// @"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE",
|
||||
// @"Table cell label")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@",
|
||||
// @"calling_hide_ip_address"]
|
||||
// isOnBlock:^{
|
||||
// return [Environment.shared.preferences doCallsHideIPAddress];
|
||||
// }
|
||||
// isEnabledBlock:^{
|
||||
// return YES;
|
||||
// }
|
||||
// target:weakSelf
|
||||
// selector:@selector(didToggleCallsHideIPAddressSwitch:)]];
|
||||
// [contents addSection:callingSection];
|
||||
//
|
||||
// if (CallUIAdapter.isCallkitDisabledForLocale) {
|
||||
// // Hide all CallKit-related prefs; CallKit is disabled.
|
||||
// } else if (@available(iOS 11, *)) {
|
||||
// OWSTableSection *callKitSection = [OWSTableSection new];
|
||||
// [callKitSection
|
||||
// addItem:[OWSTableItem switchItemWithText:NSLocalizedString(
|
||||
// @"SETTINGS_PRIVACY_CALLKIT_SYSTEM_CALL_LOG_PREFERENCE_TITLE",
|
||||
// @"Short table cell label")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"callkit_history"]
|
||||
// isOnBlock:^{
|
||||
// return [Environment.shared.preferences isSystemCallLogEnabled];
|
||||
// }
|
||||
// isEnabledBlock:^{
|
||||
// return YES;
|
||||
// }
|
||||
// target:weakSelf
|
||||
// selector:@selector(didToggleEnableSystemCallLogSwitch:)]];
|
||||
// callKitSection.footerTitle = NSLocalizedString(
|
||||
// @"SETTINGS_PRIVACY_CALLKIT_SYSTEM_CALL_LOG_PREFERENCE_DESCRIPTION", @"Settings table section footer.");
|
||||
// [contents addSection:callKitSection];
|
||||
// } else if (@available(iOS 10, *)) {
|
||||
// OWSTableSection *callKitSection = [OWSTableSection new];
|
||||
// callKitSection.footerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_SECTION_CALL_KIT_DESCRIPTION", @"Settings table section footer.");
|
||||
// [callKitSection
|
||||
// addItem:[OWSTableItem switchItemWithText:NSLocalizedString(
|
||||
// @"SETTINGS_PRIVACY_CALLKIT_TITLE", @"Short table cell label")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"callkit"]
|
||||
// isOnBlock:^{
|
||||
// return [Environment.shared.preferences isCallKitEnabled];
|
||||
// }
|
||||
// isEnabledBlock:^{
|
||||
// return YES;
|
||||
// }
|
||||
// target:weakSelf
|
||||
// selector:@selector(didToggleEnableCallKitSwitch:)]];
|
||||
// if (self.preferences.isCallKitEnabled) {
|
||||
// [callKitSection
|
||||
// addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_PRIVACY_CALLKIT_PRIVACY_TITLE",
|
||||
// @"Label for 'CallKit privacy' preference")
|
||||
// accessibilityIdentifier:[NSString
|
||||
// stringWithFormat:@"settings.privacy.%@", @"callkit_privacy"]
|
||||
// isOnBlock:^{
|
||||
// return (BOOL) ![Environment.shared.preferences isCallKitPrivacyEnabled];
|
||||
// }
|
||||
// isEnabledBlock:^{
|
||||
// return YES;
|
||||
// }
|
||||
// target:weakSelf
|
||||
// selector:@selector(didToggleEnableCallKitPrivacySwitch:)]];
|
||||
// }
|
||||
// [contents addSection:callKitSection];
|
||||
// }
|
||||
//
|
||||
// OWSTableSection *twoFactorAuthSection = [OWSTableSection new];
|
||||
// twoFactorAuthSection.headerTitle = NSLocalizedString(
|
||||
// @"SETTINGS_TWO_FACTOR_AUTH_TITLE", @"Title for the 'two factor auth' section of the privacy settings.");
|
||||
// [twoFactorAuthSection
|
||||
// addItem:
|
||||
// [OWSTableItem
|
||||
// disclosureItemWithText:NSLocalizedString(@"SETTINGS_TWO_FACTOR_AUTH_ITEM",
|
||||
// @"Label for the 'two factor auth' item of the privacy settings.")
|
||||
// detailText:
|
||||
// ([OWS2FAManager.sharedManager is2FAEnabled]
|
||||
// ? NSLocalizedString(@"SETTINGS_TWO_FACTOR_AUTH_ENABLED",
|
||||
// @"Indicates that 'two factor auth' is enabled in the privacy settings.")
|
||||
// : NSLocalizedString(@"SETTINGS_TWO_FACTOR_AUTH_DISABLED",
|
||||
// @"Indicates that 'two factor auth' is disabled in the privacy settings."))
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"2fa"]
|
||||
// actionBlock:^{
|
||||
// [weakSelf show2FASettings];
|
||||
// }]];
|
||||
// [contents addSection:twoFactorAuthSection];
|
||||
// ========
|
||||
|
||||
OWSTableSection *historyLogsSection = [OWSTableSection new];
|
||||
historyLogsSection.headerTitle = NSLocalizedString(@"SETTINGS_HISTORYLOG_TITLE", @"Section header");
|
||||
[historyLogsSection
|
||||
|
@ -297,99 +184,6 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
|
|||
}]];
|
||||
[contents addSection:historyLogsSection];
|
||||
|
||||
// Loki: Original code
|
||||
// ========
|
||||
// OWSTableSection *unidentifiedDeliveryIndicatorsSection = [OWSTableSection new];
|
||||
// unidentifiedDeliveryIndicatorsSection.headerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_SECTION_TITLE", @"table section label");
|
||||
// [unidentifiedDeliveryIndicatorsSection
|
||||
// addItem:[OWSTableItem
|
||||
// itemWithCustomCellBlock:^UITableViewCell * {
|
||||
// UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
|
||||
// reuseIdentifier:@"UITableViewCellStyleValue1"];
|
||||
// [OWSTableItem configureCell:cell];
|
||||
// cell.preservesSuperviewLayoutMargins = YES;
|
||||
// cell.contentView.preservesSuperviewLayoutMargins = YES;
|
||||
// cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
//
|
||||
// UILabel *label = [UILabel new];
|
||||
// label.text
|
||||
// = NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_SHOW_INDICATORS", @"switch label");
|
||||
// label.font = [UIFont ows_regularFontWithSize:18.f];
|
||||
// label.textColor = [Theme primaryColor];
|
||||
// [label setContentHuggingHorizontalHigh];
|
||||
//
|
||||
// UIImage *icon = [UIImage imageNamed:@"ic_secret_sender_indicator"];
|
||||
// UIImageView *iconView = [[UIImageView alloc]
|
||||
// initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
// iconView.tintColor = Theme.secondaryColor;
|
||||
// [iconView setContentHuggingHorizontalHigh];
|
||||
//
|
||||
// UIView *spacer = [UIView new];
|
||||
// [spacer setContentHuggingHorizontalLow];
|
||||
//
|
||||
// UISwitch *cellSwitch = [UISwitch new];
|
||||
// cell.accessoryView = cellSwitch;
|
||||
// [cellSwitch setOn:Environment.shared.preferences.shouldShowUnidentifiedDeliveryIndicators];
|
||||
// [cellSwitch addTarget:weakSelf
|
||||
// action:@selector(didToggleUDShowIndicatorsSwitch:)
|
||||
// forControlEvents:UIControlEventValueChanged];
|
||||
// [cellSwitch setContentHuggingHorizontalHigh];
|
||||
// cellSwitch.accessibilityIdentifier =
|
||||
// [NSString stringWithFormat:@"settings.privacy.%@", @"sealed_sender"];
|
||||
//
|
||||
// UIStackView *stackView =
|
||||
// [[UIStackView alloc] initWithArrangedSubviews:@[ label, iconView, spacer, cellSwitch ]];
|
||||
// stackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
// stackView.spacing = 10;
|
||||
// stackView.alignment = UIStackViewAlignmentCenter;
|
||||
//
|
||||
// [cell.contentView addSubview:stackView];
|
||||
// [stackView ows_autoPinToSuperviewMargins];
|
||||
// return cell;
|
||||
// }
|
||||
// customRowHeight:UITableViewAutomaticDimension
|
||||
// actionBlock:^{
|
||||
// NSURL *url = [NSURL URLWithString:kSealedSenderInfoURL];
|
||||
// OWSCAssertDebug(url);
|
||||
// [UIApplication.sharedApplication openURL:url];
|
||||
// }]];
|
||||
//
|
||||
// unidentifiedDeliveryIndicatorsSection.footerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_SHOW_INDICATORS_FOOTER", @"table section footer");
|
||||
// [contents addSection:unidentifiedDeliveryIndicatorsSection];
|
||||
//
|
||||
// OWSTableSection *unidentifiedDeliveryUnrestrictedSection = [OWSTableSection new];
|
||||
// OWSTableItem *unrestrictedAccessItem = [OWSTableItem
|
||||
// switchItemWithText:NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_UNRESTRICTED_ACCESS", @"switch label")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@", @"sealed_sender_unrestricted"]
|
||||
// isOnBlock:^{
|
||||
// return [SSKEnvironment.shared.udManager shouldAllowUnrestrictedAccessLocal];
|
||||
// }
|
||||
// isEnabledBlock:^{
|
||||
// return YES;
|
||||
// }
|
||||
// target:weakSelf
|
||||
// selector:@selector(didToggleUDUnrestrictedAccessSwitch:)];
|
||||
// [unidentifiedDeliveryUnrestrictedSection addItem:unrestrictedAccessItem];
|
||||
// unidentifiedDeliveryUnrestrictedSection.footerTitle
|
||||
// = NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_UNRESTRICTED_ACCESS_FOOTER", @"table section footer");
|
||||
// [contents addSection:unidentifiedDeliveryUnrestrictedSection];
|
||||
//
|
||||
// OWSTableSection *unidentifiedDeliveryLearnMoreSection = [OWSTableSection new];
|
||||
// [unidentifiedDeliveryLearnMoreSection
|
||||
// addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_UNIDENTIFIED_DELIVERY_LEARN_MORE",
|
||||
// @"Label for a link to more info about unidentified delivery.")
|
||||
// accessibilityIdentifier:[NSString stringWithFormat:@"settings.privacy.%@",
|
||||
// @"sealed_sender_learn_more"]
|
||||
// actionBlock:^{
|
||||
// NSURL *url = [NSURL URLWithString:kSealedSenderInfoURL];
|
||||
// OWSCAssertDebug(url);
|
||||
// [UIApplication.sharedApplication openURL:url];
|
||||
// }]];
|
||||
// [contents addSection:unidentifiedDeliveryLearnMoreSection];
|
||||
// ========
|
||||
|
||||
OWSTableSection *linkPreviewsSection = [OWSTableSection new];
|
||||
[linkPreviewsSection
|
||||
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_LINK_PREVIEWS",
|
||||
|
@ -414,12 +208,6 @@ static NSString *const kSealedSenderInfoURL = @"https://signal.org/blog/sealed-s
|
|||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)showBlocklist
|
||||
{
|
||||
BlockListViewController *vc = [BlockListViewController new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (void)clearHistoryLogs
|
||||
{
|
||||
UIAlertController *alert =
|
||||
|
|
|
@ -17,14 +17,10 @@ public enum PushRegistrationError: Error {
|
|||
/**
|
||||
* Singleton used to integrate with push notification services - registration and routing received remote notifications.
|
||||
*/
|
||||
@objc public class PushRegistrationManager: NSObject, PKPushRegistryDelegate {
|
||||
@objc public class PushRegistrationManager: NSObject {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var messageFetcherJob: MessageFetcherJob {
|
||||
return AppEnvironment.shared.messageFetcherJob
|
||||
}
|
||||
|
||||
private var notificationPresenter: NotificationPresenter {
|
||||
return AppEnvironment.shared.notificationPresenter
|
||||
}
|
||||
|
@ -91,34 +87,6 @@ public enum PushRegistrationError: Error {
|
|||
vanillaTokenResolver.reject(error)
|
||||
}
|
||||
|
||||
// MARK: PKPushRegistryDelegate - voIP Push Token
|
||||
|
||||
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
|
||||
Logger.info("")
|
||||
assert(type == .voIP)
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
(self.messageFetcherJob.run() as Promise<Void>).retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
|
||||
Logger.info("")
|
||||
assert(type == .voIP)
|
||||
assert(credentials.type == .voIP)
|
||||
guard let voipTokenResolver = self.voipTokenResolver else {
|
||||
owsFailDebug("fulfillVoipTokenPromise was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
voipTokenResolver.fulfill(credentials.token)
|
||||
}
|
||||
|
||||
public func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
||||
// It's not clear when this would happen. We've never previously handled it, but we should at
|
||||
// least start learning if it happens.
|
||||
owsFailDebug("Invalid state")
|
||||
}
|
||||
|
||||
// MARK: helpers
|
||||
|
||||
// User notification settings must be registered *before* AppDelegate will
|
||||
|
@ -200,55 +168,6 @@ public enum PushRegistrationError: Error {
|
|||
self.vanillaTokenPromise = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func registerForVoipPushToken() -> Promise<String> {
|
||||
AssertIsOnMainThread()
|
||||
Logger.info("")
|
||||
|
||||
guard self.voipTokenPromise == nil else {
|
||||
let promise = self.voipTokenPromise!
|
||||
assert(promise.isPending)
|
||||
return promise.map { $0.hexEncodedString }
|
||||
}
|
||||
|
||||
// No pending voip token yet. Create a new promise
|
||||
let (promise, resolver) = Promise<Data>.pending()
|
||||
self.voipTokenPromise = promise
|
||||
self.voipTokenResolver = resolver
|
||||
|
||||
if self.voipRegistry == nil {
|
||||
// We don't create the voip registry in init, because it immediately requests the voip token,
|
||||
// potentially before we're ready to handle it.
|
||||
let voipRegistry = PKPushRegistry(queue: nil)
|
||||
self.voipRegistry = voipRegistry
|
||||
voipRegistry.desiredPushTypes = [.voIP]
|
||||
voipRegistry.delegate = self
|
||||
}
|
||||
|
||||
guard let voipRegistry = self.voipRegistry else {
|
||||
owsFailDebug("failed to initialize voipRegistry")
|
||||
resolver.reject(PushRegistrationError.assertionError(description: "failed to initialize voipRegistry"))
|
||||
return promise.map { _ in
|
||||
// coerce expected type of returned promise - we don't really care about the value,
|
||||
// since this promise has been rejected. In practice this shouldn't happen
|
||||
String()
|
||||
}
|
||||
}
|
||||
|
||||
// If we've already completed registering for a voip token, resolve it immediately,
|
||||
// rather than waiting for the delegate method to be called.
|
||||
if let voipTokenData = voipRegistry.pushToken(for: .voIP) {
|
||||
Logger.info("using pre-registered voIP token")
|
||||
resolver.fulfill(voipTokenData)
|
||||
}
|
||||
|
||||
return promise.map { (voipTokenData: Data) -> String in
|
||||
Logger.info("successfully registered for voip push notifications")
|
||||
return voipTokenData.hexEncodedString
|
||||
}.ensure {
|
||||
self.voipTokenPromise = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We transmit pushToken data as hex encoded string to the server
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc
|
||||
public class SafetyNumberConfirmationAlert: NSObject {
|
||||
|
||||
private let contactsManager: OWSContactsManager
|
||||
private let primaryStorage: OWSPrimaryStorage
|
||||
|
||||
init(contactsManager: OWSContactsManager) {
|
||||
self.contactsManager = contactsManager
|
||||
self.primaryStorage = OWSPrimaryStorage.shared()
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func presentAlertIfNecessary(recipientId: String, confirmationText: String, contactsManager: OWSContactsManager, completion: @escaping (Bool) -> Void) -> Bool {
|
||||
return self.presentAlertIfNecessary(recipientIds: [recipientId], confirmationText: confirmationText, contactsManager: contactsManager, completion: completion, beforePresentationHandler: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func presentAlertIfNecessary(recipientId: String, confirmationText: String, contactsManager: OWSContactsManager, completion: @escaping (Bool) -> Void, beforePresentationHandler: (() -> Void)? = nil) -> Bool {
|
||||
return self.presentAlertIfNecessary(recipientIds: [recipientId], confirmationText: confirmationText, contactsManager: contactsManager, completion: completion, beforePresentationHandler: beforePresentationHandler)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func presentAlertIfNecessary(recipientIds: [String], confirmationText: String, contactsManager: OWSContactsManager, completion: @escaping (Bool) -> Void) -> Bool {
|
||||
return self.presentAlertIfNecessary(recipientIds: recipientIds, confirmationText: confirmationText, contactsManager: contactsManager, completion: completion, beforePresentationHandler: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func presentAlertIfNecessary(recipientIds: [String], confirmationText: String, contactsManager: OWSContactsManager, completion: @escaping (Bool) -> Void, beforePresentationHandler: (() -> Void)? = nil) -> Bool {
|
||||
return SafetyNumberConfirmationAlert(contactsManager: contactsManager).presentIfNecessary(recipientIds: recipientIds,
|
||||
confirmationText: confirmationText,
|
||||
completion: completion,
|
||||
beforePresentationHandler: beforePresentationHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows confirmation dialog if at least one of the recipient id's is not confirmed.
|
||||
*
|
||||
* @returns true if an alert was shown
|
||||
* false if there were no unconfirmed identities
|
||||
*/
|
||||
public func presentIfNecessary(recipientIds: [String], confirmationText: String, completion: @escaping (Bool) -> Void, beforePresentationHandler: (() -> Void)? = nil) -> Bool {
|
||||
|
||||
guard let untrustedIdentity = untrustedIdentityForSending(recipientIds: recipientIds) else {
|
||||
// No identities to confirm, no alert to present.
|
||||
return false
|
||||
}
|
||||
|
||||
let displayName = contactsManager.displayName(forPhoneIdentifier: untrustedIdentity.recipientId)
|
||||
|
||||
let titleFormat = NSLocalizedString("CONFIRM_SENDING_TO_CHANGED_IDENTITY_TITLE_FORMAT",
|
||||
comment: "Action sheet title presented when a user's SN has recently changed. Embeds {{contact's name or phone number}}")
|
||||
let title = String(format: titleFormat, displayName)
|
||||
|
||||
let bodyFormat = NSLocalizedString("CONFIRM_SENDING_TO_CHANGED_IDENTITY_BODY_FORMAT",
|
||||
comment: "Action sheet body presented when a user's SN has recently changed. Embeds {{contact's name or phone number}}")
|
||||
let body = String(format: bodyFormat, displayName)
|
||||
|
||||
let actionSheet = UIAlertController(title: title, message: body, preferredStyle: .actionSheet)
|
||||
|
||||
let confirmAction = UIAlertAction(title: confirmationText, style: .default) { _ in
|
||||
Logger.info("Confirmed identity: \(untrustedIdentity)")
|
||||
|
||||
self.primaryStorage.newDatabaseConnection().asyncReadWrite { (transaction) in
|
||||
OWSIdentityManager.shared().setVerificationState(.default, identityKey: untrustedIdentity.identityKey, recipientId: untrustedIdentity.recipientId, isUserInitiatedChange: true, transaction: transaction)
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
actionSheet.addAction(confirmAction)
|
||||
|
||||
let showSafetyNumberAction = UIAlertAction(title: NSLocalizedString("VERIFY_PRIVACY", comment: "Label for button or row which allows users to verify the safety number of another user."), style: .default) { _ in
|
||||
Logger.info("Opted to show Safety Number for identity: \(untrustedIdentity)")
|
||||
|
||||
self.presentSafetyNumberViewController(theirIdentityKey: untrustedIdentity.identityKey,
|
||||
theirRecipientId: untrustedIdentity.recipientId,
|
||||
theirDisplayName: displayName,
|
||||
completion: { completion(false) })
|
||||
|
||||
}
|
||||
actionSheet.addAction(showSafetyNumberAction)
|
||||
|
||||
// We can't use the default `OWSAlerts.cancelAction` because we need to specify that the completion
|
||||
// handler is called.
|
||||
let cancelAction = UIAlertAction(title: CommonStrings.cancelButton, style: .cancel) { _ in
|
||||
Logger.info("user canceled.")
|
||||
completion(false)
|
||||
}
|
||||
actionSheet.addAction(cancelAction)
|
||||
|
||||
beforePresentationHandler?()
|
||||
|
||||
UIApplication.shared.frontmostViewController?.presentAlert(actionSheet)
|
||||
return true
|
||||
}
|
||||
|
||||
public func presentSafetyNumberViewController(theirIdentityKey: Data, theirRecipientId: String, theirDisplayName: String, completion: (() -> Void)? = nil) {
|
||||
guard let fromViewController = UIApplication.shared.frontmostViewController else {
|
||||
Logger.info("Missing frontmostViewController")
|
||||
return
|
||||
}
|
||||
FingerprintViewController.present(from: fromViewController, recipientId: theirRecipientId)
|
||||
}
|
||||
|
||||
private func untrustedIdentityForSending(recipientIds: [String]) -> OWSRecipientIdentity? {
|
||||
return recipientIds.compactMap {
|
||||
OWSIdentityManager.shared().untrustedIdentityForSending(toRecipientId: $0)
|
||||
}.first
|
||||
}
|
||||
}
|
|
@ -97,10 +97,6 @@ public class SessionResetOperation: OWSOperation, DurableOperation {
|
|||
return SSKEnvironment.shared.primaryStorage
|
||||
}
|
||||
|
||||
var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
var firstAttempt = true
|
||||
|
@ -108,19 +104,7 @@ public class SessionResetOperation: OWSOperation, DurableOperation {
|
|||
override public func run() {
|
||||
assert(self.durableOperationDelegate != nil)
|
||||
|
||||
/* Loki: Original code
|
||||
* We don't want to delete the session. Ref: SignalServiceKit/Loki/Docs/SessionReset.md
|
||||
* ================
|
||||
if firstAttempt {
|
||||
Storage.writeSync { transaction in
|
||||
Logger.info("deleting sessions for recipient: \(self.recipientId)")
|
||||
self.primaryStorage.deleteAllSessions(forContact: self.recipientId, protocolContext: transaction)
|
||||
}
|
||||
firstAttempt = false
|
||||
}
|
||||
* ================
|
||||
*/
|
||||
|
||||
/*
|
||||
let endSessionMessage = EndSessionMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread)
|
||||
|
||||
firstly {
|
||||
|
@ -157,6 +141,7 @@ public class SessionResetOperation: OWSOperation, DurableOperation {
|
|||
Logger.error("sending error: \(error.localizedDescription)")
|
||||
self.reportError(error)
|
||||
}.retainUntilComplete()
|
||||
*/
|
||||
}
|
||||
|
||||
override public func didSucceed() {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSTableViewController.h"
|
||||
|
||||
@class TSGroupThread;
|
||||
|
||||
@interface ShowGroupMembersViewController : OWSTableViewController
|
||||
|
||||
- (void)configWithThread:(TSGroupThread *)thread;
|
||||
|
||||
@end
|
|
@ -1,478 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ShowGroupMembersViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SignalUtilitiesKit/BlockListUIUtils.h>
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/ContactsViewHelper.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
|
||||
@import ContactsUI;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ShowGroupMembersViewController () <ContactsViewHelperDelegate, ContactEditingDelegate>
|
||||
|
||||
@property (nonatomic, readonly) TSGroupThread *thread;
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic, nullable) NSSet<NSString *> *memberRecipientIds;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ShowGroupMembersViewController
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
|
||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableView.estimatedRowHeight = 60;
|
||||
|
||||
[self observeNotifications];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(identityStateDidChange:)
|
||||
name:kNSNotificationName_IdentityStateDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)configWithThread:(TSGroupThread *)thread
|
||||
{
|
||||
|
||||
_thread = thread;
|
||||
|
||||
OWSAssertDebug(self.thread);
|
||||
OWSAssertDebug(self.thread.groupModel);
|
||||
OWSAssertDebug(self.thread.groupModel.groupMemberIds);
|
||||
|
||||
self.memberRecipientIds = [NSSet setWithArray:self.thread.groupModel.groupMemberIds];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
OWSAssertDebug([self.navigationController isKindOfClass:[OWSNavigationController class]]);
|
||||
|
||||
self.title = _thread.groupModel.groupName;
|
||||
|
||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableView.estimatedRowHeight = 45;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSAssertDebug(self.thread);
|
||||
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak ShowGroupMembersViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
|
||||
OWSTableSection *membersSection = [OWSTableSection new];
|
||||
|
||||
// Group Members
|
||||
|
||||
// If there are "no longer verified" members of the group,
|
||||
// highlight them in a special section.
|
||||
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
|
||||
if (noLongerVerifiedRecipientIds.count > 0) {
|
||||
OWSTableSection *noLongerVerifiedSection = [OWSTableSection new];
|
||||
noLongerVerifiedSection.headerTitle = NSLocalizedString(@"GROUP_MEMBERS_SECTION_TITLE_NO_LONGER_VERIFIED",
|
||||
@"Title for the 'no longer verified' section of the 'group members' view.");
|
||||
membersSection.headerTitle = NSLocalizedString(
|
||||
@"GROUP_MEMBERS_SECTION_TITLE_MEMBERS", @"Title for the 'members' section of the 'group members' view.");
|
||||
[noLongerVerifiedSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"GROUP_MEMBERS_RESET_NO_LONGER_VERIFIED",
|
||||
@"Label for the button that clears all verification "
|
||||
@"errors in the 'group members' view.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"no_longer_verified")
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[weakSelf offerResetAllNoLongerVerified];
|
||||
}]];
|
||||
[self addMembers:noLongerVerifiedRecipientIds toSection:noLongerVerifiedSection useVerifyAction:YES];
|
||||
[contents addSection:noLongerVerifiedSection];
|
||||
}
|
||||
|
||||
NSMutableSet *memberRecipientIds = [self.memberRecipientIds mutableCopy];
|
||||
[memberRecipientIds removeObject:[helper localNumber]];
|
||||
[self addMembers:memberRecipientIds.allObjects toSection:membersSection useVerifyAction:NO];
|
||||
[contents addSection:membersSection];
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)addMembers:(NSArray<NSString *> *)recipientIds
|
||||
toSection:(OWSTableSection *)section
|
||||
useVerifyAction:(BOOL)useVerifyAction
|
||||
{
|
||||
OWSAssertDebug(recipientIds);
|
||||
OWSAssertDebug(section);
|
||||
|
||||
__weak ShowGroupMembersViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
// Sort the group members using contacts manager.
|
||||
NSArray<NSString *> *sortedRecipientIds = [recipientIds sortedArrayUsingComparator:^NSComparisonResult(
|
||||
NSString *recipientIdA, NSString *recipientIdB) {
|
||||
SignalAccount *signalAccountA = [helper.contactsManager fetchOrBuildSignalAccountForRecipientId:recipientIdA];
|
||||
SignalAccount *signalAccountB = [helper.contactsManager fetchOrBuildSignalAccountForRecipientId:recipientIdB];
|
||||
return [helper.contactsManager compareSignalAccount:signalAccountA withSignalAccount:signalAccountB];
|
||||
}];
|
||||
for (NSString *recipientId in sortedRecipientIds) {
|
||||
[section addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
ShowGroupMembersViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
OWSVerificationState verificationState =
|
||||
[[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId];
|
||||
BOOL isVerified = verificationState == OWSVerificationStateVerified;
|
||||
BOOL isNoLongerVerified = verificationState == OWSVerificationStateNoLongerVerified;
|
||||
BOOL isBlocked = [helper isRecipientIdBlocked:recipientId];
|
||||
if (isNoLongerVerified) {
|
||||
cell.accessoryMessage = NSLocalizedString(@"CONTACT_CELL_IS_NO_LONGER_VERIFIED",
|
||||
@"An indicator that a contact is no longer verified.");
|
||||
} else if (isBlocked) {
|
||||
cell.accessoryMessage = NSLocalizedString(
|
||||
@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked.");
|
||||
}
|
||||
|
||||
[cell configureWithRecipientId:recipientId];
|
||||
|
||||
if (isVerified) {
|
||||
[cell setAttributedSubtitle:cell.verifiedSubtitle];
|
||||
} else {
|
||||
[cell setAttributedSubtitle:nil];
|
||||
}
|
||||
|
||||
NSString *cellName = [NSString stringWithFormat:@"user.%@", recipientId];
|
||||
cell.accessibilityIdentifier
|
||||
= ACCESSIBILITY_IDENTIFIER_WITH_NAME(ShowGroupMembersViewController, cellName);
|
||||
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
if (useVerifyAction) {
|
||||
[weakSelf showSafetyNumberView:recipientId];
|
||||
} else {
|
||||
[weakSelf didSelectRecipientId:recipientId];
|
||||
}
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)offerResetAllNoLongerVerified
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
UIAlertController *actionSheet = [UIAlertController
|
||||
alertControllerWithTitle:nil
|
||||
message:NSLocalizedString(@"GROUP_MEMBERS_RESET_NO_LONGER_VERIFIED_ALERT_MESSAGE",
|
||||
@"Label for the 'reset all no-longer-verified group members' confirmation alert.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
__weak ShowGroupMembersViewController *weakSelf = self;
|
||||
UIAlertAction *verifyAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[weakSelf resetAllNoLongerVerified];
|
||||
}];
|
||||
[actionSheet addAction:verifyAction];
|
||||
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[self presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
- (void)resetAllNoLongerVerified
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSIdentityManager *identityManger = [OWSIdentityManager sharedManager];
|
||||
NSArray<NSString *> *recipientIds = [self noLongerVerifiedRecipientIds];
|
||||
for (NSString *recipientId in recipientIds) {
|
||||
OWSVerificationState verificationState = [identityManger verificationStateForRecipientId:recipientId];
|
||||
if (verificationState == OWSVerificationStateNoLongerVerified) {
|
||||
NSData *identityKey = [identityManger identityKeyForRecipientId:recipientId];
|
||||
if (identityKey.length < 1) {
|
||||
OWSFailDebug(@"Missing identity key for: %@", recipientId);
|
||||
continue;
|
||||
}
|
||||
[identityManger setVerificationState:OWSVerificationStateDefault
|
||||
identityKey:identityKey
|
||||
recipientId:recipientId
|
||||
isUserInitiatedChange:YES];
|
||||
}
|
||||
}
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
// Returns a collection of the group members who are "no longer verified".
|
||||
- (NSArray<NSString *> *)noLongerVerifiedRecipientIds
|
||||
{
|
||||
NSMutableArray<NSString *> *result = [NSMutableArray new];
|
||||
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
||||
if ([[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId]
|
||||
== OWSVerificationStateNoLongerVerified) {
|
||||
[result addObject:recipientId];
|
||||
}
|
||||
}
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
- (void)didSelectRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
SignalAccount *_Nullable signalAccount = [helper fetchSignalAccountForRecipientId:recipientId];
|
||||
|
||||
UIAlertController *actionSheet =
|
||||
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
if (self.contactsViewHelper.contactsManager.supportsContactEditing) {
|
||||
NSString *contactInfoTitle = signalAccount
|
||||
? NSLocalizedString(@"GROUP_MEMBERS_VIEW_CONTACT_INFO", @"Button label for the 'show contact info' button")
|
||||
: NSLocalizedString(
|
||||
@"GROUP_MEMBERS_ADD_CONTACT_INFO", @"Button label to add information to an unknown contact");
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:contactInfoTitle
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"show_contact_info")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self showContactInfoViewForRecipientId:recipientId];
|
||||
}]];
|
||||
}
|
||||
|
||||
BOOL isBlocked;
|
||||
if (signalAccount) {
|
||||
isBlocked = [helper isRecipientIdBlocked:signalAccount.recipientId];
|
||||
if (isBlocked) {
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_LIST_UNBLOCK_BUTTON",
|
||||
@"Button label for the 'unblock' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"unblock")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils
|
||||
showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL ignore) {
|
||||
[self updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
} else {
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_LIST_BLOCK_BUTTON",
|
||||
@"Button label for the 'block' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"block")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils
|
||||
showBlockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL ignore) {
|
||||
[self updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
} else {
|
||||
isBlocked = [helper isRecipientIdBlocked:recipientId];
|
||||
if (isBlocked) {
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_LIST_UNBLOCK_BUTTON",
|
||||
@"Button label for the 'unblock' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"unblock")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils
|
||||
showUnblockPhoneNumberActionSheet:recipientId
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL ignore) {
|
||||
[self updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
} else {
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_LIST_BLOCK_BUTTON",
|
||||
@"Button label for the 'block' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"block")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils
|
||||
showBlockPhoneNumberActionSheet:recipientId
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL ignore) {
|
||||
[self updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlocked) {
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"GROUP_MEMBERS_SEND_MESSAGE",
|
||||
@"Button label for the 'send message to group member' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_message")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self showConversationViewForRecipientId:recipientId];
|
||||
}]];
|
||||
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"GROUP_MEMBERS_CALL",
|
||||
@"Button label for the 'call group member' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"call")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self callMember:recipientId];
|
||||
}]];
|
||||
[actionSheet
|
||||
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"VERIFY_PRIVACY",
|
||||
@"Label for button or row which allows users to verify the "
|
||||
@"safety number of another user.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"safety_numbers")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self showSafetyNumberView:recipientId];
|
||||
}]];
|
||||
}
|
||||
|
||||
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[self presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
- (void)showContactInfoViewForRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[self.contactsViewHelper presentContactViewControllerForRecipientId:recipientId
|
||||
fromViewController:self
|
||||
editImmediately:NO];
|
||||
}
|
||||
|
||||
- (void)showConversationViewForRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[SignalApp.sharedApp presentConversationForRecipientId:recipientId
|
||||
action:ConversationViewActionCompose
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
- (void)callMember:(NSString *)recipientId
|
||||
{
|
||||
[SignalApp.sharedApp presentConversationForRecipientId:recipientId
|
||||
action:ConversationViewActionAudioCall
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
- (void)showSafetyNumberView:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[FingerprintViewController presentFromViewController:self recipientId:recipientId];
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - ContactEditingDelegate
|
||||
|
||||
- (void)didFinishEditingContact
|
||||
{
|
||||
OWSLogDebug(@"");
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - CNContactViewControllerDelegate
|
||||
|
||||
- (void)contactViewController:(CNContactViewController *)viewController
|
||||
didCompleteWithContact:(nullable CNContact *)contact
|
||||
{
|
||||
OWSLogDebug(@"done editing contact.");
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)identityStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -6,7 +6,6 @@
|
|||
#import "AppDelegate.h"
|
||||
#import "ConversationViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalsNavigationController.h"
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/DebugLogger.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
|
@ -178,8 +177,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (void)showHomeView
|
||||
{
|
||||
HomeVC *homeView = [HomeVC new];
|
||||
SignalsNavigationController *navigationController =
|
||||
[[SignalsNavigationController alloc] initWithRootViewController:homeView];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:homeView];
|
||||
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
appDelegate.window.rootViewController = navigationController;
|
||||
OWSAssertDebug([navigationController.topViewController isKindOfClass:[HomeVC class]]);
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSNavigationController.h"
|
||||
|
||||
@interface SignalsNavigationController : OWSNavigationController
|
||||
|
||||
@end
|
|
@ -1,123 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SignalsNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
|
||||
static double const STALLED_PROGRESS = 0.9;
|
||||
|
||||
@interface SignalsNavigationController ()
|
||||
|
||||
@property (nonatomic) UIProgressView *socketStatusView;
|
||||
@property (nonatomic) NSTimer *updateStatusTimer;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation SignalsNavigationController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
[self initializeObserver];
|
||||
[self updateSocketStatusView];
|
||||
}
|
||||
|
||||
- (void)initializeSocketStatusBar {
|
||||
if (!_socketStatusView) {
|
||||
_socketStatusView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
|
||||
}
|
||||
|
||||
CGRect bar = self.navigationBar.frame;
|
||||
_socketStatusView.frame = CGRectMake(0, bar.size.height - 1.0f, self.view.frame.size.width, 1.0f);
|
||||
_socketStatusView.progress = 0.0f;
|
||||
_socketStatusView.progressTintColor = [UIColor ows_fadedBlueColor];
|
||||
|
||||
/** Loki: Original code
|
||||
if (![_socketStatusView superview]) {
|
||||
[self.navigationBar addSubview:_socketStatusView];
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Socket Status Notifications
|
||||
|
||||
- (void)initializeObserver {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(OWSWebSocketStateDidChange)
|
||||
name:kNSNotification_OWSWebSocketStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(isCensorshipCircumventionActiveDidChange:)
|
||||
name:kNSNotificationName_IsCensorshipCircumventionActiveDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)isCensorshipCircumventionActiveDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateSocketStatusView];
|
||||
}
|
||||
|
||||
- (void)OWSWebSocketStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateSocketStatusView];
|
||||
}
|
||||
|
||||
- (void)updateSocketStatusView {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
if ([OWSSignalService sharedInstance].isCensorshipCircumventionActive) {
|
||||
[_updateStatusTimer invalidate];
|
||||
[_socketStatusView removeFromSuperview];
|
||||
_socketStatusView = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (TSSocketManager.shared.highestSocketState) {
|
||||
case OWSWebSocketStateClosed:
|
||||
if (_socketStatusView == nil) {
|
||||
[self initializeSocketStatusBar];
|
||||
[_updateStatusTimer invalidate];
|
||||
_updateStatusTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.5
|
||||
target:self
|
||||
selector:@selector(updateProgress)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
} else if (_socketStatusView.progress >= STALLED_PROGRESS) {
|
||||
[_updateStatusTimer invalidate];
|
||||
}
|
||||
break;
|
||||
case OWSWebSocketStateConnecting:
|
||||
// Do nothing.
|
||||
break;
|
||||
case OWSWebSocketStateOpen:
|
||||
[_updateStatusTimer invalidate];
|
||||
[_socketStatusView removeFromSuperview];
|
||||
_socketStatusView = nil;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateProgress {
|
||||
double progress = _socketStatusView.progress + 0.05;
|
||||
_socketStatusView.progress = (float) MIN(progress, STALLED_PROGRESS);
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSConversationSettingsViewDelegate.h"
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSGroupThread;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, UpdateGroupMode) {
|
||||
UpdateGroupMode_Default = 0,
|
||||
UpdateGroupMode_EditGroupName,
|
||||
UpdateGroupMode_EditGroupAvatar,
|
||||
};
|
||||
|
||||
@interface UpdateGroupViewController : OWSViewController
|
||||
|
||||
@property (nonatomic, weak) id<OWSConversationSettingsViewDelegate> conversationSettingsViewDelegate;
|
||||
|
||||
// This property _must_ be set before the view is presented.
|
||||
@property (nonatomic) TSGroupThread *thread;
|
||||
|
||||
@property (nonatomic) UpdateGroupMode mode;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,558 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UpdateGroupViewController.h"
|
||||
#import "AddToGroupViewController.h"
|
||||
#import "AvatarViewHelper.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
#import <SignalUtilitiesKit/BlockListUIUtils.h>
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/ContactsViewHelper.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
#import <SignalUtilitiesKit/SignalKeyingStorage.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIViewController+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UpdateGroupViewController () <UIImagePickerControllerDelegate,
|
||||
UITextFieldDelegate,
|
||||
ContactsViewHelperDelegate,
|
||||
AvatarViewHelperDelegate,
|
||||
AddToGroupViewControllerDelegate,
|
||||
OWSTableViewControllerDelegate,
|
||||
UINavigationControllerDelegate,
|
||||
OWSNavigationView>
|
||||
|
||||
@property (nonatomic, readonly) OWSMessageSender *messageSender;
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
@property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
@property (nonatomic, readonly) AvatarImageView *avatarView;
|
||||
@property (nonatomic, readonly) UITextField *groupNameTextField;
|
||||
|
||||
@property (nonatomic, nullable) UIImage *groupAvatar;
|
||||
@property (nonatomic, nullable) NSSet<NSString *> *previousMemberRecipientIds;
|
||||
@property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds;
|
||||
@property (nonatomic) NSMutableSet<NSString *> *removedRecipientIds;
|
||||
|
||||
@property (nonatomic) BOOL hasUnsavedChanges;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation UpdateGroupViewController
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
_messageSender = SSKEnvironment.shared.messageSender;
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
_avatarViewHelper = [AvatarViewHelper new];
|
||||
_avatarViewHelper.delegate = self;
|
||||
|
||||
self.memberRecipientIds = [NSMutableSet new];
|
||||
self.removedRecipientIds = [NSMutableSet new];
|
||||
}
|
||||
|
||||
#pragma mark - View Lifecycle
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
OWSAssertDebug(self.thread);
|
||||
OWSAssertDebug(self.thread.groupModel);
|
||||
OWSAssertDebug(self.thread.groupModel.groupMemberIds);
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
[self.memberRecipientIds addObjectsFromArray:self.thread.groupModel.groupMemberIds];
|
||||
self.previousMemberRecipientIds = [NSSet setWithArray:self.thread.groupModel.groupMemberIds];
|
||||
|
||||
self.title = NSLocalizedString(@"EDIT_GROUP_DEFAULT_TITLE", @"The navbar title for the 'update group' view.");
|
||||
|
||||
// First section.
|
||||
|
||||
UIView *firstSection = [self firstSectionHeader];
|
||||
[self.view addSubview:firstSection];
|
||||
[firstSection autoSetDimension:ALDimensionHeight toSize:100.f];
|
||||
[firstSection autoPinWidthToSuperview];
|
||||
[firstSection autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
_tableViewController.delegate = self;
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:firstSection];
|
||||
[self autoPinViewToBottomOfViewControllerOrKeyboard:self.tableViewController.view avoidNotch:NO];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)setHasUnsavedChanges:(BOOL)hasUnsavedChanges
|
||||
{
|
||||
_hasUnsavedChanges = hasUnsavedChanges;
|
||||
|
||||
[self updateNavigationBar];
|
||||
}
|
||||
|
||||
- (void)updateNavigationBar
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem = (self.hasUnsavedChanges
|
||||
? [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON",
|
||||
@"The title for the 'update group' button.")
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(updateGroupPressed)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"update")]
|
||||
: nil);
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
switch (self.mode) {
|
||||
case UpdateGroupMode_EditGroupName:
|
||||
[self.groupNameTextField becomeFirstResponder];
|
||||
break;
|
||||
case UpdateGroupMode_EditGroupAvatar:
|
||||
[self showChangeAvatarUI];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Only perform these actions the first time the view appears.
|
||||
self.mode = UpdateGroupMode_Default;
|
||||
}
|
||||
|
||||
- (UIView *)firstSectionHeader
|
||||
{
|
||||
OWSAssertDebug(self.thread);
|
||||
OWSAssertDebug(self.thread.groupModel);
|
||||
|
||||
UIView *firstSectionHeader = [UIView new];
|
||||
firstSectionHeader.userInteractionEnabled = YES;
|
||||
[firstSectionHeader
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(headerWasTapped:)]];
|
||||
firstSectionHeader.backgroundColor = [Theme backgroundColor];
|
||||
UIView *threadInfoView = [UIView new];
|
||||
[firstSectionHeader addSubview:threadInfoView];
|
||||
[threadInfoView autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[threadInfoView autoPinHeightToSuperviewWithMargin:16.f];
|
||||
|
||||
AvatarImageView *avatarView = [AvatarImageView new];
|
||||
_avatarView = avatarView;
|
||||
|
||||
[threadInfoView addSubview:avatarView];
|
||||
[avatarView autoVCenterInSuperview];
|
||||
[avatarView autoPinLeadingToSuperviewMargin];
|
||||
[avatarView autoSetDimension:ALDimensionWidth toSize:kLargeAvatarSize];
|
||||
[avatarView autoSetDimension:ALDimensionHeight toSize:kLargeAvatarSize];
|
||||
_groupAvatar = self.thread.groupModel.groupImage;
|
||||
[self updateAvatarView];
|
||||
|
||||
UITextField *groupNameTextField = [OWSTextField new];
|
||||
_groupNameTextField = groupNameTextField;
|
||||
self.groupNameTextField.text = [self.thread.groupModel.groupName ows_stripped];
|
||||
groupNameTextField.textColor = [Theme primaryColor];
|
||||
groupNameTextField.font = [UIFont ows_dynamicTypeTitle2Font];
|
||||
groupNameTextField.placeholder
|
||||
= NSLocalizedString(@"NEW_GROUP_NAMEGROUP_REQUEST_DEFAULT", @"Placeholder text for group name field");
|
||||
groupNameTextField.delegate = self;
|
||||
[groupNameTextField addTarget:self
|
||||
action:@selector(groupNameDidChange:)
|
||||
forControlEvents:UIControlEventEditingChanged];
|
||||
[threadInfoView addSubview:groupNameTextField];
|
||||
[groupNameTextField autoVCenterInSuperview];
|
||||
[groupNameTextField autoPinTrailingToSuperviewMargin];
|
||||
[groupNameTextField autoPinLeadingToTrailingEdgeOfView:avatarView offset:16.f];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, groupNameTextField);
|
||||
|
||||
[avatarView
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarTouched:)]];
|
||||
avatarView.userInteractionEnabled = YES;
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, avatarView);
|
||||
|
||||
return firstSectionHeader;
|
||||
}
|
||||
|
||||
- (void)headerWasTapped:(UIGestureRecognizer *)sender
|
||||
{
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.groupNameTextField becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)avatarTouched:(UIGestureRecognizer *)sender
|
||||
{
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showChangeAvatarUI];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSAssertDebug(self.thread);
|
||||
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak UpdateGroupViewController *weakSelf = self;
|
||||
ContactsViewHelper *contactsViewHelper = self.contactsViewHelper;
|
||||
|
||||
// Group Members
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
section.headerTitle = NSLocalizedString(
|
||||
@"EDIT_GROUP_MEMBERS_SECTION_TITLE", @"a title for the members section of the 'new/update group' view.");
|
||||
|
||||
[section addItem:[OWSTableItem
|
||||
disclosureItemWithText:NSLocalizedString(@"EDIT_GROUP_MEMBERS_ADD_MEMBER",
|
||||
@"Label for the cell that lets you add a new member to a group.")
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
AddToGroupViewController *viewController = [AddToGroupViewController new];
|
||||
viewController.addToGroupDelegate = weakSelf;
|
||||
[weakSelf.navigationController pushViewController:viewController animated:YES];
|
||||
}]];
|
||||
|
||||
NSMutableSet *memberRecipientIds = [self.memberRecipientIds mutableCopy];
|
||||
[memberRecipientIds removeObject:[contactsViewHelper localNumber]];
|
||||
for (NSString *recipientId in [memberRecipientIds.allObjects sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[section
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UpdateGroupViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
BOOL isPreviousMember = [strongSelf.previousMemberRecipientIds containsObject:recipientId];
|
||||
BOOL isBlocked = [contactsViewHelper isRecipientIdBlocked:recipientId];
|
||||
if (isPreviousMember) {
|
||||
if (isBlocked) {
|
||||
cell.accessoryMessage = NSLocalizedString(
|
||||
@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked.");
|
||||
} else {
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
}
|
||||
} else {
|
||||
// In the "members" section, we label "new" members as such when editing an existing
|
||||
// group.
|
||||
//
|
||||
// The only way a "new" member could be blocked is if we blocked them on a linked device
|
||||
// while in this dialog. We don't need to worry about that edge case.
|
||||
cell.accessoryMessage = NSLocalizedString(@"EDIT_GROUP_NEW_MEMBER_LABEL",
|
||||
@"An indicator that a user is a new member of the group.");
|
||||
}
|
||||
|
||||
[cell configureWithRecipientId:recipientId];
|
||||
|
||||
// TODO: Confirm with nancy if this will work.
|
||||
NSString *cellName = [NSString stringWithFormat:@"member.%@", recipientId];
|
||||
cell.accessibilityIdentifier
|
||||
= ACCESSIBILITY_IDENTIFIER_WITH_NAME(UpdateGroupViewController, cellName);
|
||||
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
SignalAccount *_Nullable signalAccount =
|
||||
[contactsViewHelper fetchSignalAccountForRecipientId:recipientId];
|
||||
BOOL isPreviousMember = [weakSelf.previousMemberRecipientIds containsObject:recipientId];
|
||||
BOOL isBlocked = [contactsViewHelper isRecipientIdBlocked:recipientId];
|
||||
if (isPreviousMember) {
|
||||
if (isBlocked) {
|
||||
if (signalAccount) {
|
||||
[weakSelf showUnblockAlertForSignalAccount:signalAccount];
|
||||
} else {
|
||||
[weakSelf showUnblockAlertForRecipientId:recipientId];
|
||||
}
|
||||
} else {
|
||||
[OWSAlerts
|
||||
showAlertWithTitle:
|
||||
NSLocalizedString(@"UPDATE_GROUP_CANT_REMOVE_MEMBERS_ALERT_TITLE",
|
||||
@"Title for alert indicating that group members can't be removed.")
|
||||
message:NSLocalizedString(
|
||||
@"UPDATE_GROUP_CANT_REMOVE_MEMBERS_ALERT_MESSAGE",
|
||||
@"Title for alert indicating that group members can't "
|
||||
@"be removed.")];
|
||||
}
|
||||
} else {
|
||||
[weakSelf removeRecipientId:recipientId];
|
||||
}
|
||||
}]];
|
||||
}
|
||||
[contents addSection:section];
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)showUnblockAlertForSignalAccount:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak UpdateGroupViewController *weakSelf = self;
|
||||
[BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:self.contactsViewHelper.blockingManager
|
||||
contactsManager:self.contactsViewHelper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showUnblockAlertForRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
__weak UpdateGroupViewController *weakSelf = self;
|
||||
[BlockListUIUtils showUnblockPhoneNumberActionSheet:recipientId
|
||||
fromViewController:self
|
||||
blockingManager:self.contactsViewHelper.blockingManager
|
||||
contactsManager:self.contactsViewHelper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)removeRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[self.memberRecipientIds removeObject:recipientId];
|
||||
[self.removedRecipientIds addObject:recipientId];
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Methods
|
||||
|
||||
- (void)updateGroup
|
||||
{
|
||||
OWSAssertDebug(self.conversationSettingsViewDelegate);
|
||||
|
||||
NSString *groupName = [self.groupNameTextField.text ows_stripped];
|
||||
TSGroupModel *groupModel = [[TSGroupModel alloc] initWithTitle:groupName
|
||||
memberIds:self.memberRecipientIds.allObjects
|
||||
image:self.groupAvatar
|
||||
groupId:self.thread.groupModel.groupId
|
||||
groupType:self.thread.groupModel.groupType
|
||||
adminIds:self.thread.groupModel.groupAdminIds];
|
||||
[self.conversationSettingsViewDelegate groupWasUpdated:groupModel];
|
||||
}
|
||||
|
||||
#pragma mark - Group Avatar
|
||||
|
||||
- (void)showChangeAvatarUI
|
||||
{
|
||||
[self.groupNameTextField resignFirstResponder];
|
||||
|
||||
[self.avatarViewHelper showChangeAvatarUI];
|
||||
}
|
||||
|
||||
- (void)setGroupAvatar:(nullable UIImage *)groupAvatar
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
_groupAvatar = groupAvatar;
|
||||
|
||||
self.hasUnsavedChanges = YES;
|
||||
|
||||
[self updateAvatarView];
|
||||
}
|
||||
|
||||
- (void)updateAvatarView
|
||||
{
|
||||
UIImage *_Nullable groupAvatar = self.groupAvatar;
|
||||
if (!groupAvatar) {
|
||||
groupAvatar = [[[OWSGroupAvatarBuilder alloc] initWithThread:self.thread diameter:kLargeAvatarSize] build];
|
||||
}
|
||||
self.avatarView.image = groupAvatar;
|
||||
}
|
||||
|
||||
#pragma mark - Event Handling
|
||||
|
||||
- (void)backButtonPressed
|
||||
{
|
||||
[self.groupNameTextField resignFirstResponder];
|
||||
|
||||
if (!self.hasUnsavedChanges) {
|
||||
// If user made no changes, return to conversation settings view.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
return;
|
||||
}
|
||||
|
||||
UIAlertController *alert = [UIAlertController
|
||||
alertControllerWithTitle:NSLocalizedString(@"EDIT_GROUP_VIEW_UNSAVED_CHANGES_TITLE",
|
||||
@"The alert title if user tries to exit update group view without saving changes.")
|
||||
message:
|
||||
NSLocalizedString(@"EDIT_GROUP_VIEW_UNSAVED_CHANGES_MESSAGE",
|
||||
@"The alert message if user tries to exit update group view without saving changes.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_SAVE",
|
||||
@"The label for the 'save' button in action sheets.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"save")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
OWSAssertDebug(self.conversationSettingsViewDelegate);
|
||||
|
||||
[self updateGroup];
|
||||
|
||||
[self.conversationSettingsViewDelegate
|
||||
popAllConversationSettingsViewsWithCompletion:nil];
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DONT_SAVE",
|
||||
@"The label for the 'don't save' button in action sheets.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dont_save")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *action) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}]];
|
||||
[self presentAlert:alert];
|
||||
}
|
||||
|
||||
- (void)updateGroupPressed
|
||||
{
|
||||
OWSAssertDebug(self.conversationSettingsViewDelegate);
|
||||
|
||||
[self updateGroup];
|
||||
|
||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViewsWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)groupNameDidChange:(id)sender
|
||||
{
|
||||
self.hasUnsavedChanges = YES;
|
||||
}
|
||||
|
||||
#pragma mark - Text Field Delegate
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField
|
||||
{
|
||||
[self.groupNameTextField resignFirstResponder];
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - OWSTableViewControllerDelegate
|
||||
|
||||
- (void)tableViewWillBeginDragging
|
||||
{
|
||||
[self.groupNameTextField resignFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - AvatarViewHelperDelegate
|
||||
|
||||
- (nullable NSString *)avatarActionSheetTitle
|
||||
{
|
||||
return NSLocalizedString(
|
||||
@"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar");
|
||||
}
|
||||
|
||||
- (void)avatarDidChange:(UIImage *)image
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(image);
|
||||
|
||||
self.groupAvatar = image;
|
||||
}
|
||||
|
||||
- (UIViewController *)fromViewController
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)hasClearAvatarAction
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - AddToGroupViewControllerDelegate
|
||||
|
||||
- (void)recipientIdWasAdded:(NSString *)recipientId
|
||||
{
|
||||
[self.memberRecipientIds addObject:recipientId];
|
||||
self.hasUnsavedChanges = YES;
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)isRecipientGroupMember:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
return [self.memberRecipientIds containsObject:recipientId];
|
||||
}
|
||||
|
||||
#pragma mark - OWSNavigationView
|
||||
|
||||
- (BOOL)shouldCancelNavigationBack
|
||||
{
|
||||
BOOL result = self.hasUnsavedChanges;
|
||||
if (result) {
|
||||
[self backButtonPressed];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -125,7 +125,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
|
||||
let trigger: UNNotificationTrigger?
|
||||
let checkForCancel = category == .incomingMessage
|
||||
if checkForCancel && hasReceivedSyncMessageRecently {
|
||||
if checkForCancel {
|
||||
assert(userInfo[AppNotificationUserInfoKey.threadId] != nil)
|
||||
trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForRemoteRead, repeats: false)
|
||||
} else {
|
||||
|
|
|
@ -21,61 +21,8 @@ public class WebRTCCallMessageHandler: NSObject/*, OWSCallMessageHandler*/ {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var messageSender : MessageSender
|
||||
{
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
private var accountManager : AccountManager
|
||||
{
|
||||
return AppEnvironment.shared.accountManager
|
||||
}
|
||||
|
||||
// private var callService : CallService
|
||||
// {
|
||||
// return AppEnvironment.shared.callService
|
||||
// }
|
||||
|
||||
// MARK: - Call Handlers
|
||||
|
||||
// public func receivedOffer(_ offer: SSKProtoCallMessageOffer, from callerId: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
// self.callService.handleReceivedOffer(thread: thread, callId: offer.id, sessionDescription: offer.sessionDescription)
|
||||
// }
|
||||
//
|
||||
// public func receivedAnswer(_ answer: SSKProtoCallMessageAnswer, from callerId: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
// self.callService.handleReceivedAnswer(thread: thread, callId: answer.id, sessionDescription: answer.sessionDescription)
|
||||
// }
|
||||
//
|
||||
// public func receivedIceUpdate(_ iceUpdate: SSKProtoCallMessageIceUpdate, from callerId: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
//
|
||||
// // Discrepency between our protobuf's sdpMlineIndex, which is unsigned,
|
||||
// // while the RTC iOS API requires a signed int.
|
||||
// let lineIndex = Int32(iceUpdate.sdpMlineIndex)
|
||||
//
|
||||
// self.callService.handleRemoteAddedIceCandidate(thread: thread, callId: iceUpdate.id, sdp: iceUpdate.sdp, lineIndex: lineIndex, mid: iceUpdate.sdpMid)
|
||||
// }
|
||||
//
|
||||
// public func receivedHangup(_ hangup: SSKProtoCallMessageHangup, from callerId: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
// self.callService.handleRemoteHangup(thread: thread, callId: hangup.id)
|
||||
// }
|
||||
//
|
||||
// public func receivedBusy(_ busy: SSKProtoCallMessageBusy, from callerId: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: callerId)
|
||||
// self.callService.handleRemoteBusy(thread: thread, callId: busy.id)
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ public final class BackgroundPoller : NSObject {
|
|||
@objc(pollWithCompletionHandler:)
|
||||
public static func poll(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
var promises: [Promise<Void>] = []
|
||||
promises.append(AppEnvironment.shared.messageFetcherJob.run()) // FIXME: It'd be nicer to just use Poller directly
|
||||
// TODO TODO TODO
|
||||
// promises.append(AppEnvironment.shared.messageFetcherJob.run()) // FIXME: It'd be nicer to just use Poller directly
|
||||
closedGroupPoller = ClosedGroupPoller()
|
||||
promises.append(contentsOf: closedGroupPoller.pollOnce())
|
||||
var openGroups: [String:OpenGroup] = [:]
|
||||
|
|
|
@ -24,7 +24,6 @@ class BaseVC : UIViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleUnexpectedDeviceLinkRequestReceivedNotification(_:)), name: .unexpectedDeviceLinkRequestReceived, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleAppModeChangedNotification(_:)), name: .appModeChanged, object: nil)
|
||||
}
|
||||
|
||||
|
@ -71,15 +70,6 @@ class BaseVC : UIViewController {
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func handleUnexpectedDeviceLinkRequestReceivedNotification(_ notification: Notification) {
|
||||
guard DeviceLinkingUtilities.shouldShowUnexpectedDeviceLinkRequestReceivedAlert else { return }
|
||||
DispatchQueue.main.async {
|
||||
let alert = UIAlertController(title: "Device Link Request Received", message: "Open the device link screen by going to \"Settings\" > \"Devices\" > \"Link a Device\" to link your devices.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// TODO: Post an appModeChanged notification?
|
||||
}
|
||||
|
|
|
@ -1,256 +0,0 @@
|
|||
import NVActivityIndicatorView
|
||||
|
||||
@objc(LKDeviceLinkingModal)
|
||||
final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
||||
private let mode: Mode
|
||||
private let delegate: DeviceLinkingModalDelegate?
|
||||
private var deviceLink: DeviceLink?
|
||||
private var hasAuthorizedDeviceLink = false
|
||||
|
||||
// MARK: Types
|
||||
enum Mode : String { case master, slave }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var spinner = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
|
||||
|
||||
private lazy var qrCodeImageViewContainer: UIView = {
|
||||
let result = UIView()
|
||||
result.addSubview(qrCodeImageView)
|
||||
qrCodeImageView.pin(.top, to: .top, of: result)
|
||||
qrCodeImageView.pin(.bottom, to: .bottom, of: result)
|
||||
qrCodeImageView.center(.horizontal, in: result)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var qrCodeImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
let size: CGFloat = 128
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var buttonStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ cancelButton, authorizeButton ])
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.distribution = .fillEqually
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var authorizeButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.set(.height, to: Values.mediumButtonHeight)
|
||||
result.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
result.backgroundColor = Colors.accent
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
result.setTitle(NSLocalizedString("modal_link_device_master_mode_authorize_button_title", comment: ""), for: UIControl.State.normal)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mainStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel, mnemonicLabel, buttonStackView ])
|
||||
result.spacing = Values.largeSpacing
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(mode: Mode, delegate: DeviceLinkingModalDelegate?) {
|
||||
self.mode = mode
|
||||
if mode == .slave {
|
||||
guard delegate != nil else { preconditionFailure("Missing delegate for device linking modal in slave mode.") }
|
||||
}
|
||||
self.delegate = delegate
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@objc(initWithMode:delegate:)
|
||||
convenience init(modeAsString: String, delegate: DeviceLinkingModalDelegate?) {
|
||||
guard let mode = Mode(rawValue: modeAsString) else { preconditionFailure("Invalid mode: \(modeAsString).") }
|
||||
self.init(mode: mode, delegate: delegate)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { preconditionFailure() }
|
||||
override init(nibName: String?, bundle: Bundle?) { preconditionFailure() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
switch mode {
|
||||
case .master: let _ = DeviceLinkingSession.startListeningForLinkingRequests(with: self)
|
||||
case .slave: let _ = DeviceLinkingSession.startListeningForLinkingAuthorization(with: self)
|
||||
}
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
switch mode {
|
||||
case .master: mainStackView.insertArrangedSubview(qrCodeImageViewContainer, at: 0)
|
||||
case .slave: mainStackView.insertArrangedSubview(spinner, at: 0)
|
||||
}
|
||||
contentView.addSubview(mainStackView)
|
||||
switch mode {
|
||||
case .master:
|
||||
let hexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
qrCodeImageView.image = QRCode.generate(for: hexEncodedPublicKey, hasBackground: true)
|
||||
case .slave:
|
||||
spinner.set(.height, to: 64)
|
||||
spinner.startAnimating()
|
||||
}
|
||||
titleLabel.text = {
|
||||
switch mode {
|
||||
case .master: return NSLocalizedString("modal_link_device_master_mode_title_1", comment: "")
|
||||
case .slave: return NSLocalizedString("modal_link_device_slave_mode_title_1", comment: "")
|
||||
}
|
||||
}()
|
||||
subtitleLabel.text = {
|
||||
switch mode {
|
||||
case .master: return NSLocalizedString("modal_link_device_master_mode_explanation_1", comment: "")
|
||||
case .slave: return NSLocalizedString("modal_link_device_slave_mode_explanation_1", comment: "")
|
||||
}
|
||||
}()
|
||||
mnemonicLabel.isHidden = (mode == .master)
|
||||
if mode == .slave {
|
||||
let hexEncodedPublicKey = getUserHexEncodedPublicKey().removing05PrefixIfNeeded()
|
||||
mnemonicLabel.text = Mnemonic.hash(hexEncodedString: hexEncodedPublicKey)
|
||||
}
|
||||
authorizeButton.addTarget(self, action: #selector(authorizeDeviceLink), for: UIControl.Event.touchUpInside)
|
||||
authorizeButton.isHidden = true
|
||||
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Device Linking
|
||||
func requestUserAuthorization(for deviceLink: DeviceLink) {
|
||||
self.deviceLink = deviceLink
|
||||
qrCodeImageViewContainer.isHidden = true
|
||||
titleLabel.text = NSLocalizedString("modal_link_device_master_mode_title_2", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("modal_link_device_master_mode_explanation_2", comment: "")
|
||||
let hexEncodedPublicKey = deviceLink.slave.publicKey.removing05PrefixIfNeeded()
|
||||
mnemonicLabel.text = Mnemonic.hash(hexEncodedString: hexEncodedPublicKey)
|
||||
mnemonicLabel.isHidden = false
|
||||
authorizeButton.isHidden = false
|
||||
}
|
||||
|
||||
@objc private func authorizeDeviceLink() {
|
||||
guard !hasAuthorizedDeviceLink else { return }
|
||||
hasAuthorizedDeviceLink = true
|
||||
mainStackView.removeArrangedSubview(qrCodeImageViewContainer)
|
||||
mainStackView.insertArrangedSubview(spinner, at: 0)
|
||||
spinner.set(.height, to: 64)
|
||||
spinner.startAnimating()
|
||||
titleLabel.text = NSLocalizedString("modal_link_device_master_mode_title_3", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("modal_link_device_master_mode_explanation_3", comment: "")
|
||||
mnemonicLabel.isHidden = true
|
||||
buttonStackView.isHidden = true
|
||||
let deviceLink = self.deviceLink!
|
||||
DeviceLinkingSession.current!.markLinkingRequestAsProcessed()
|
||||
DeviceLinkingSession.current!.stopListeningForLinkingRequests()
|
||||
let linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(for: deviceLink)
|
||||
let master = DeviceLink.Device(publicKey: deviceLink.master.publicKey, signature: linkingAuthorizationMessage.masterSignature)
|
||||
let signedDeviceLink = DeviceLink(between: master, and: deviceLink.slave)
|
||||
FileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
|
||||
SSKEnvironment.shared.messageSender.send(linkingAuthorizationMessage, success: {
|
||||
let slavePublicKey = deviceLink.slave.publicKey
|
||||
Storage.writeSync { transaction in
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: slavePublicKey, transaction: transaction)
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
let _ = SSKEnvironment.shared.syncManager.syncAllGroups().ensure {
|
||||
// Closed groups first because we prefer the session request mechanism
|
||||
// to the AFR mechanism
|
||||
let _ = SSKEnvironment.shared.syncManager.syncAllContacts()
|
||||
}
|
||||
let _ = SSKEnvironment.shared.syncManager.syncAllOpenGroups()
|
||||
DispatchQueue.main.async {
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
self?.delegate?.handleDeviceLinkAuthorized(signedDeviceLink)
|
||||
}
|
||||
}, failure: { error in
|
||||
print("[Loki] Failed to send device link authorization message.")
|
||||
let _ = FileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
|
||||
DispatchQueue.main.async {
|
||||
self?.close()
|
||||
let alert = UIAlertController(title: "Device Linking Failed", message: "Please check your internet connection and try again", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self?.presentingViewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
})
|
||||
}.catch { [weak self] error in
|
||||
print("[Loki] Failed to add device link due to error: \(error).")
|
||||
DispatchQueue.main.async {
|
||||
self?.close() // TODO: Show a message to the user
|
||||
let alert = UIAlertController(title: "Device Linking Failed", message: "Please check your internet connection and try again", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self?.presentingViewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
let session = DeviceLinkingSession.current!
|
||||
session.stopListeningForLinkingAuthorization()
|
||||
spinner.stopAnimating()
|
||||
spinner.isHidden = true
|
||||
titleLabel.text = NSLocalizedString("modal_link_device_slave_mode_title_2", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("modal_link_device_slave_mode_explanation_2", comment: "")
|
||||
mnemonicLabel.isHidden = true
|
||||
buttonStackView.isHidden = true
|
||||
FileServerAPI.addDeviceLink(deviceLink).catch { error in
|
||||
print("[Loki] Failed to add device link due to error: \(error).")
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
self.dismiss(animated: true) {
|
||||
self.delegate?.handleDeviceLinkAuthorized(deviceLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc override func close() {
|
||||
guard let session = DeviceLinkingSession.current else {
|
||||
return print("[Loki] Device linking session missing.") // Should never occur
|
||||
}
|
||||
session.stopListeningForLinkingRequests()
|
||||
session.markLinkingRequestAsProcessed() // Only relevant in master mode
|
||||
delegate?.handleDeviceLinkingModalDismissed() // Only relevant in slave mode
|
||||
if let deviceLink = deviceLink {
|
||||
Storage.writeSync { transaction in
|
||||
OWSPrimaryStorage.shared().removePreKeyBundle(forContact: deviceLink.slave.publicKey, transaction: transaction)
|
||||
}
|
||||
}
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
@objc protocol DeviceLinkingModalDelegate {
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink)
|
||||
func handleDeviceLinkingModalDismissed()
|
||||
}
|
||||
|
||||
extension DeviceLinkingModalDelegate {
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) { /* Do nothing */ }
|
||||
func handleDeviceLinkingModalDismissed() { /* Do nothing */ }
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
|
||||
// MARK: - Device Links View Controller
|
||||
|
||||
@objc(LKDeviceLinksVC)
|
||||
final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate, DeviceLinkingModalDelegate, DeviceNameModalDelegate {
|
||||
private var deviceLinks: [DeviceLink] = [] { didSet { updateUI() } }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tableView: UITableView = {
|
||||
let result = UITableView()
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.register(Cell.self, forCellReuseIdentifier: "Cell")
|
||||
result.separatorStyle = .none
|
||||
result.backgroundColor = .clear
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var callToActionView : UIStackView = {
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.text = NSLocalizedString("vc_linked_devices_empty_state_message", comment: "")
|
||||
let linkNewDeviceButton = Button(style: .prominentOutline, size: .large)
|
||||
linkNewDeviceButton.setTitle(NSLocalizedString("vc_linked_devices_empty_state_button_title", comment: ""), for: UIControl.State.normal)
|
||||
linkNewDeviceButton.addTarget(self, action: #selector(linkNewDevice), for: UIControl.Event.touchUpInside)
|
||||
linkNewDeviceButton.set(.width, to: 196)
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel, linkNewDeviceButton ])
|
||||
result.axis = .vertical
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.alignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setUpGradientBackground()
|
||||
setUpNavBarStyle()
|
||||
setNavBarTitle(NSLocalizedString("vc_linked_devices_title", comment: ""))
|
||||
// Set up link new device button
|
||||
let linkNewDeviceButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(linkNewDevice))
|
||||
linkNewDeviceButton.tintColor = Colors.text
|
||||
navigationItem.rightBarButtonItem = linkNewDeviceButton
|
||||
// Set up constraints
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(to: view)
|
||||
view.addSubview(callToActionView)
|
||||
callToActionView.center(.horizontal, in: view)
|
||||
let verticalCenteringConstraint = callToActionView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
// Perform initial update
|
||||
updateDeviceLinks()
|
||||
}
|
||||
|
||||
// MARK: Data
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return deviceLinks.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
||||
cell.selectedBackgroundView = selectedBackgroundView
|
||||
let device = deviceLinks[indexPath.row].other
|
||||
cell.device = device
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func updateDeviceLinks() {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
var deviceLinks: [DeviceLink] = []
|
||||
storage.dbReadConnection.read { transaction in
|
||||
deviceLinks = storage.getDeviceLinks(for: userHexEncodedPublicKey, in: transaction).sorted { lhs, rhs in
|
||||
return lhs.other.publicKey > rhs.other.publicKey
|
||||
}
|
||||
}
|
||||
self.deviceLinks = deviceLinks
|
||||
}
|
||||
|
||||
private func updateUI() {
|
||||
tableView.reloadData()
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.callToActionView.isHidden = !self.deviceLinks.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
// The modal already dismisses itself
|
||||
updateDeviceLinks()
|
||||
}
|
||||
|
||||
func handleDeviceLinkingModalDismissed() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func linkNewDevice() {
|
||||
if deviceLinks.isEmpty {
|
||||
let deviceLinkingModal = DeviceLinkingModal(mode: .master, delegate: self)
|
||||
deviceLinkingModal.modalPresentationStyle = .overFullScreen
|
||||
deviceLinkingModal.modalTransitionStyle = .crossDissolve
|
||||
present(deviceLinkingModal, animated: true, completion: nil)
|
||||
} else {
|
||||
let alert = UIAlertController(title: NSLocalizedString("vc_linked_devices_multi_device_limit_reached_modal_title", comment: ""), message: NSLocalizedString("It's currently not allowed to link more than one device.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
defer { tableView.deselectRow(at: indexPath, animated: true) }
|
||||
let deviceLink = deviceLinks[indexPath.row]
|
||||
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
sheet.addAction(UIAlertAction(title: NSLocalizedString("vc_device_list_bottom_sheet_change_name_button_title", comment: ""), style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let deviceNameModal = DeviceNameModal()
|
||||
deviceNameModal.device = deviceLink.other
|
||||
deviceNameModal.delegate = self
|
||||
deviceNameModal.modalPresentationStyle = .overFullScreen
|
||||
deviceNameModal.modalTransitionStyle = .crossDissolve
|
||||
self.present(deviceNameModal, animated: true, completion: nil)
|
||||
})
|
||||
sheet.addAction(UIAlertAction(title: NSLocalizedString("vc_device_list_bottom_sheet_unlink_device_button_title", comment: ""), style: .destructive) { [weak self] _ in
|
||||
self?.removeDeviceLink(deviceLink)
|
||||
})
|
||||
sheet.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in })
|
||||
present(sheet, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func handleDeviceNameChanged(to name: String, for device: DeviceLink.Device) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
updateUI()
|
||||
}
|
||||
|
||||
private func removeDeviceLink(_ deviceLink: DeviceLink) {
|
||||
FileServerAPI.removeDeviceLink(deviceLink).done { [weak self] in
|
||||
let linkedDevicePublicKey = deviceLink.other.publicKey
|
||||
guard let thread = TSContactThread.fetch(uniqueId: TSContactThread.threadId(fromContactId: linkedDevicePublicKey)) else { return }
|
||||
let unlinkDeviceMessage = UnlinkDeviceMessage(thread: thread)
|
||||
SSKEnvironment.shared.messageSender.send(unlinkDeviceMessage, success: {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
Storage.writeSync { transaction in
|
||||
storage.removePreKeyBundle(forContact: linkedDevicePublicKey, transaction: transaction)
|
||||
storage.deleteAllSessions(forContact: linkedDevicePublicKey, protocolContext: transaction)
|
||||
for groupPublicKey in Storage.getUserClosedGroupPublicKeys() {
|
||||
// TODO: Possibly re-implement in the future
|
||||
// ClosedGroupsProtocol.removeMembers([ linkedDevicePublicKey ], from: groupPublicKey, using: transaction)
|
||||
}
|
||||
}
|
||||
}, failure: { _ in
|
||||
print("[Loki] Failed to send unlink device message.")
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
Storage.writeSync { transaction in
|
||||
storage.removePreKeyBundle(forContact: linkedDevicePublicKey, transaction: transaction)
|
||||
storage.deleteAllSessions(forContact: linkedDevicePublicKey, protocolContext: transaction)
|
||||
for groupPublicKey in Storage.getUserClosedGroupPublicKeys() {
|
||||
// TODO: Possibly re-implement in the future
|
||||
// ClosedGroupsProtocol.removeMembers([ linkedDevicePublicKey ], from: groupPublicKey, using: transaction)
|
||||
}
|
||||
}
|
||||
})
|
||||
self?.updateDeviceLinks()
|
||||
}.catch { [weak self] _ in
|
||||
let alert = UIAlertController(title: "Couldn't Unlink Device", message: "Please check your internet connection and try again", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cell
|
||||
|
||||
private extension DeviceLinksVC {
|
||||
|
||||
final class Cell : UITableViewCell {
|
||||
var device: DeviceLink.Device! { didSet { update() } }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.cellBackground
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ])
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.spacing = Values.verySmallSpacing
|
||||
stackView.set(.height, to: 44)
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: 12)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 12)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
private func update() {
|
||||
titleLabel.text = device.displayName
|
||||
subtitleLabel.text = Mnemonic.hash(hexEncodedString: device.publicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
|
||||
@objc(LKDeviceNameModal)
|
||||
final class DeviceNameModal : Modal {
|
||||
@objc public var device: DeviceLink.Device!
|
||||
@objc public var delegate: DeviceNameModalDelegate?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var nameTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("modal_edit_device_name_text_field_hint", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Colors.text.withAlphaComponent(Values.unimportantElementOpacity), range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = Colors.accent
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = "Change Device Name"
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.textAlignment = .center
|
||||
// Set up explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = "Enter the new display name for your device below"
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Set up OK button
|
||||
let okButton = UIButton()
|
||||
okButton.set(.height, to: Values.mediumButtonHeight)
|
||||
okButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
okButton.backgroundColor = Colors.accent
|
||||
okButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
okButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
okButton.setTitle(NSLocalizedString("OK", comment: ""), for: UIControl.State.normal)
|
||||
okButton.addTarget(self, action: #selector(changeName), for: UIControl.Event.touchUpInside)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, okButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
// Set up main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, nameTextField, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
||||
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
|
||||
verticalCenteringConstraint.constant = -(newHeight / 2)
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
||||
verticalCenteringConstraint.constant = 0
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func changeName() {
|
||||
let name = nameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if !name.isEmpty {
|
||||
UserDefaults.standard[.slaveDeviceName(device.publicKey)] = name
|
||||
delegate?.handleDeviceNameChanged(to: name, for: device)
|
||||
} else {
|
||||
let alert = UIAlertController(title: "Error", message: "Please pick a name", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
@objc protocol DeviceNameModalDelegate {
|
||||
|
||||
func handleDeviceNameChanged(to name: String, for device: DeviceLink.Device)
|
||||
}
|
|
@ -96,8 +96,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
// Set up seed reminder view if needed
|
||||
let userDefaults = UserDefaults.standard
|
||||
let hasViewedSeed = userDefaults[.hasViewedSeed]
|
||||
let isMasterDevice = userDefaults.isMasterDevice
|
||||
if !hasViewedSeed && isMasterDevice {
|
||||
if !hasViewedSeed {
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
seedReminderView.pin(.top, to: .top, of: view)
|
||||
|
@ -108,7 +107,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
tableView.delegate = self
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(.leading, to: .leading, of: view)
|
||||
if !hasViewedSeed && isMasterDevice {
|
||||
if !hasViewedSeed {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
||||
} else {
|
||||
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
||||
|
@ -283,13 +282,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.size = profilePictureSize
|
||||
let userHexEncodedPublicKey: String
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
userHexEncodedPublicKey = masterHexEncodedPublicKey
|
||||
} else {
|
||||
userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
}
|
||||
profilePictureView.hexEncodedPublicKey = userHexEncodedPublicKey
|
||||
profilePictureView.hexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
profilePictureView.update()
|
||||
profilePictureView.set(.width, to: profilePictureSize)
|
||||
profilePictureView.set(.height, to: profilePictureSize)
|
||||
|
@ -413,10 +406,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
if thread is TSContactThread {
|
||||
var publicKey: String!
|
||||
Storage.read { transaction in
|
||||
publicKey = OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: thread.contactIdentifier()!, in: transaction) ?? thread.contactIdentifier()!
|
||||
}
|
||||
let publicKey = thread.contactIdentifier()!
|
||||
let blockingManager = SSKEnvironment.shared.blockingManager
|
||||
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
|
||||
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
|
||||
|
|
|
@ -133,7 +133,7 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
|
|||
isJoining = true
|
||||
let channelID: UInt64 = 1
|
||||
let urlAsString = url.absoluteString
|
||||
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let profileManager = OWSProfileManager.shared()
|
||||
let displayName = profileManager.profileNameForRecipient(withID: userPublicKey)
|
||||
let profilePictureURL = profileManager.profilePictureURL()
|
||||
|
@ -148,8 +148,6 @@ final class JoinPublicChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
|
|||
let _ = OpenGroupAPI.setDisplayName(to: displayName, on: urlAsString)
|
||||
let _ = OpenGroupAPI.setProfilePictureURL(to: profilePictureURL, using: profileKey, on: urlAsString)
|
||||
let _ = OpenGroupAPI.join(channelID, on: urlAsString)
|
||||
let syncManager = SSKEnvironment.shared.syncManager
|
||||
let _ = syncManager.syncAllOpenGroups()
|
||||
self?.presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
.catch(on: DispatchQueue.main) { [weak self] error in
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate {
|
||||
final class LandingVC : BaseVC {
|
||||
private var fakeChatViewContentOffset: CGPoint!
|
||||
|
||||
// MARK: Components
|
||||
|
@ -25,14 +25,6 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton: Button = {
|
||||
let result = Button(style: .regularBorderless, size: .small)
|
||||
result.setTitle(NSLocalizedString("vc_landing_link_button_title", comment: ""), for: UIControl.State.normal)
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.addTarget(self, action: #selector(linkDevice), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -59,11 +51,6 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
|
|||
// Set up link button container
|
||||
let linkButtonContainer = UIView()
|
||||
linkButtonContainer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
// linkButtonContainer.addSubview(linkButton)
|
||||
// linkButton.pin(.leading, to: .leading, of: linkButtonContainer, withInset: Values.massiveSpacing)
|
||||
// linkButton.pin(.top, to: .top, of: linkButtonContainer)
|
||||
// linkButtonContainer.pin(.trailing, to: .trailing, of: linkButton, withInset: Values.massiveSpacing)
|
||||
// linkButtonContainer.pin(.bottom, to: .bottom, of: linkButton, withInset: isIPhone5OrSmaller ? 6 : 10)
|
||||
// Set up button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ registerButton, restoreButton ])
|
||||
buttonStackView.axis = .vertical
|
||||
|
@ -83,13 +70,6 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
|
|||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(to: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Show device unlinked alert if needed
|
||||
if UserDefaults.standard[.wasUnlinked] {
|
||||
let alert = UIAlertController(title: "Device Unlinked", message: NSLocalizedString("vc_landing_device_unlinked_modal_title", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
UserDefaults.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
|
@ -118,58 +98,9 @@ final class LandingVC : BaseVC, LinkDeviceVCDelegate, DeviceLinkingModalDelegate
|
|||
navigationController!.pushViewController(restoreVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func linkDevice() {
|
||||
let linkDeviceVC = LinkDeviceVC()
|
||||
linkDeviceVC.delegate = self
|
||||
let navigationController = OWSNavigationController(rootViewController: linkDeviceVC)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Device Linking
|
||||
func requestDeviceLink(with hexEncodedPublicKey: String) {
|
||||
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) else {
|
||||
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: "Please make sure the Session ID you entered is correct and try again.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
}
|
||||
let seed = Randomness.generateRandomBytes(16)
|
||||
preconditionFailure("This code path shouldn't be invoked.")
|
||||
let keyPair = Curve25519.generateKeyPair()
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
|
||||
databaseConnection.setObject(seed.toHexString(), forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.startPollerIfNeeded()
|
||||
let deviceLinkingModal = DeviceLinkingModal(mode: .slave, delegate: self)
|
||||
deviceLinkingModal.modalPresentationStyle = .overFullScreen
|
||||
deviceLinkingModal.modalTransitionStyle = .crossDissolve
|
||||
self.present(deviceLinkingModal, animated: true, completion: nil)
|
||||
let linkingRequestMessage = DeviceLinkingUtilities.getLinkingRequestMessage(for: hexEncodedPublicKey)
|
||||
ThreadUtil.enqueue(linkingRequestMessage)
|
||||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
UserDefaults.standard[.masterHexEncodedPublicKey] = deviceLink.master.publicKey
|
||||
fakeChatViewContentOffset = fakeChatView.contentOffset
|
||||
DispatchQueue.main.async {
|
||||
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
|
||||
}
|
||||
let homeVC = HomeVC()
|
||||
navigationController!.setViewControllers([ homeVC ], animated: true)
|
||||
}
|
||||
|
||||
func handleDeviceLinkingModalDismissed() {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopPoller()
|
||||
TSAccountManager.sharedInstance().resetForReregistration()
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func setUserInteractionEnabled(_ isEnabled: Bool) {
|
||||
[ registerButton, restoreButton, linkButton ].forEach {
|
||||
[ registerButton, restoreButton ].forEach {
|
||||
$0.isUserInteractionEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,14 +149,6 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
@objc private func createClosedGroup() {
|
||||
if ClosedGroupsProtocol.isSharedSenderKeysEnabled {
|
||||
createSSKClosedGroup()
|
||||
} else {
|
||||
createLegacyClosedGroup()
|
||||
}
|
||||
}
|
||||
|
||||
private func createSSKClosedGroup() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
|
@ -176,16 +168,15 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
let selectedContacts = self.selectedContacts
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
|
||||
FileServerAPI.getDeviceLinks(associatedWith: selectedContacts).then2 { _ -> Promise<TSGroupThread> in
|
||||
var promise: Promise<TSGroupThread>!
|
||||
Storage.writeSync { transaction in
|
||||
promise = ClosedGroupsProtocol.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
|
||||
}
|
||||
return promise
|
||||
}.done(on: DispatchQueue.main) { thread in
|
||||
var promise: Promise<TSGroupThread>!
|
||||
Storage.writeSync { transaction in
|
||||
promise = ClosedGroupsProtocol.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
|
||||
}
|
||||
let _ = promise.done(on: DispatchQueue.main) { thread in
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}.catch(on: DispatchQueue.main) { _ in
|
||||
}
|
||||
promise.catch(on: DispatchQueue.main) { _ in
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
let title = "Couldn't Create Group"
|
||||
let message = "Please check your internet connection and try again."
|
||||
|
@ -195,57 +186,6 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createLegacyClosedGroup() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else {
|
||||
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: ""))
|
||||
}
|
||||
guard name.count < 64 else {
|
||||
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: ""))
|
||||
}
|
||||
guard selectedContacts.count >= 1 else {
|
||||
return showError(title: "Please pick at least 1 group member")
|
||||
}
|
||||
guard selectedContacts.count < 10 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 userPublicKey = getUserHexEncodedPublicKey()
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
var masterPublicKey = ""
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterPublicKey = storage.getMasterHexEncodedPublicKey(for: userPublicKey, in: transaction) ?? userPublicKey
|
||||
}
|
||||
let members = selectedContacts + [ masterPublicKey ]
|
||||
let admins = [ masterPublicKey ]
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(Randomness.generateRandomBytes(kGroupIdLength).toHexString())
|
||||
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group)
|
||||
OWSProfileManager.shared().addThread(toProfileWhitelist: thread)
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
let message = TSOutgoingMessage(in: thread, groupMetaMessage: .new, expiresInSeconds: 0)
|
||||
message.update(withCustomMessage: "Closed group created")
|
||||
DispatchQueue.main.async {
|
||||
SSKEnvironment.shared.messageSender.send(message, success: {
|
||||
DispatchQueue.main.async {
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
}, failure: { error in
|
||||
let message = TSErrorMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, failedMessageType: .groupCreationFailed)
|
||||
message.save()
|
||||
DispatchQueue.main.async {
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func createNewPrivateChat() {
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
|
|
|
@ -147,14 +147,6 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
|
|||
private final class EnterPublicKeyVC : UIViewController {
|
||||
weak var newPrivateChatVC: NewPrivateChatVC!
|
||||
|
||||
private lazy var userPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Components
|
||||
private let publicKeyTextView = TextView(placeholder: NSLocalizedString("vc_enter_public_key_text_field_hint", comment: ""))
|
||||
|
||||
|
@ -186,7 +178,7 @@ private final class EnterPublicKeyVC : UIViewController {
|
|||
userPublicKeyLabel.numberOfLines = 0
|
||||
userPublicKeyLabel.textAlignment = .center
|
||||
userPublicKeyLabel.lineBreakMode = .byCharWrapping
|
||||
userPublicKeyLabel.text = userPublicKey
|
||||
userPublicKeyLabel.text = getUserHexEncodedPublicKey()
|
||||
// Set up share button
|
||||
let shareButton = Button(style: .unimportant, size: .medium)
|
||||
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
|
||||
|
@ -239,7 +231,7 @@ private final class EnterPublicKeyVC : UIViewController {
|
|||
|
||||
// MARK: Interaction
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = userPublicKey
|
||||
UIPasteboard.general.string = getUserHexEncodedPublicKey()
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
|
||||
|
@ -248,7 +240,7 @@ private final class EnterPublicKeyVC : UIViewController {
|
|||
}
|
||||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ userPublicKey ], applicationActivities: nil)
|
||||
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
|
||||
newPrivateChatVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -137,14 +137,6 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
weak var qrCodeVC: QRCodeVC!
|
||||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
|
@ -160,7 +152,7 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
titleLabel.set(.height, to: isIPhone5OrSmaller ? CGFloat(40) : Values.massiveFontSize)
|
||||
// Set up QR code image view
|
||||
let qrCodeImageView = UIImageView()
|
||||
let qrCode = QRCode.generate(for: userHexEncodedPublicKey, hasBackground: true)
|
||||
let qrCode = QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: true)
|
||||
qrCodeImageView.image = qrCode
|
||||
qrCodeImageView.contentMode = .scaleAspectFit
|
||||
qrCodeImageView.set(.height, to: isIPhone5OrSmaller ? 180 : 240)
|
||||
|
@ -218,7 +210,7 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
|
||||
// MARK: Interaction
|
||||
@objc private func shareQRCode() {
|
||||
let qrCode = QRCode.generate(for: userHexEncodedPublicKey, hasBackground: true)
|
||||
let qrCode = QRCode.generate(for: getUserHexEncodedPublicKey(), hasBackground: true)
|
||||
let shareVC = UIActivityViewController(activityItems: [ qrCode ], applicationActivities: nil)
|
||||
qrCodeVC.navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
|
|
@ -4,14 +4,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
private var displayNameToBeUploaded: String?
|
||||
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: Components
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
let result = ProfilePictureView()
|
||||
|
@ -71,10 +63,10 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
// Set up profile picture view
|
||||
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
|
||||
profilePictureView.addGestureRecognizer(profilePictureTapGestureRecognizer)
|
||||
profilePictureView.hexEncodedPublicKey = userHexEncodedPublicKey
|
||||
profilePictureView.hexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
profilePictureView.update()
|
||||
// Set up display name label
|
||||
displayNameLabel.text = OWSProfileManager.shared().profileNameForRecipient(withID: userHexEncodedPublicKey)
|
||||
displayNameLabel.text = OWSProfileManager.shared().profileNameForRecipient(withID: getUserHexEncodedPublicKey())
|
||||
// Set up display name container
|
||||
let displayNameContainer = UIView()
|
||||
displayNameContainer.addSubview(displayNameLabel)
|
||||
|
@ -99,7 +91,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
publicKeyLabel.numberOfLines = 0
|
||||
publicKeyLabel.textAlignment = .center
|
||||
publicKeyLabel.lineBreakMode = .byCharWrapping
|
||||
publicKeyLabel.text = userHexEncodedPublicKey
|
||||
publicKeyLabel.text = getUserHexEncodedPublicKey()
|
||||
// Set up share button
|
||||
let shareButton = Button(style: .regular, size: .medium)
|
||||
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
|
||||
|
@ -178,23 +170,19 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
button.set(.height, to: Values.settingButtonHeight)
|
||||
return button
|
||||
}
|
||||
var result = [
|
||||
return [
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: "Invite", color: Colors.text, action: #selector(sendInvitation))
|
||||
getSettingButton(withTitle: "Invite", color: Colors.text, action: #selector(sendInvitation)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)),
|
||||
getSeparator()
|
||||
]
|
||||
let isMasterDevice = UserDefaults.standard.isMasterDevice
|
||||
if isMasterDevice {
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)))
|
||||
}
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)))
|
||||
result.append(getSeparator())
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
|
@ -283,8 +271,8 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
}
|
||||
|
||||
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
|
||||
let displayName = displayNameToBeUploaded ?? OWSProfileManager.shared().profileNameForRecipient(withID: userHexEncodedPublicKey)
|
||||
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: userHexEncodedPublicKey)
|
||||
let displayName = displayNameToBeUploaded ?? OWSProfileManager.shared().profileNameForRecipient(withID: getUserHexEncodedPublicKey())
|
||||
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: getUserHexEncodedPublicKey())
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: profilePicture, success: {
|
||||
DispatchQueue.main.async {
|
||||
|
@ -372,7 +360,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
}
|
||||
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = userHexEncodedPublicKey
|
||||
UIPasteboard.general.string = getUserHexEncodedPublicKey()
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
|
||||
|
@ -381,7 +369,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
}
|
||||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ userHexEncodedPublicKey ], applicationActivities: nil)
|
||||
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -394,14 +382,9 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
let notificationSettingsVC = NotificationSettingsViewController()
|
||||
navigationController!.pushViewController(notificationSettingsVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showLinkedDevices() {
|
||||
let deviceLinksVC = DeviceLinksVC()
|
||||
navigationController!.pushViewController(deviceLinksVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func sendInvitation() {
|
||||
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(userHexEncodedPublicKey)!"
|
||||
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(getUserHexEncodedPublicKey())!"
|
||||
let shareVC = UIActivityViewController(activityItems: [ invitation ], applicationActivities: nil)
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue