From d05df87dd2cf8ee9b17eb1b6b44b53fb90f9df76 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 11 Dec 2019 16:08:08 +1100 Subject: [PATCH] Implement the much anticipated Simon status bar --- Signal.xcodeproj/project.pbxproj | 2 +- .../Components/ConversationTitleView.swift | 167 ++++++++++++++++++ .../Loki/Redesign/Style Guide/Values.swift | 1 + .../ConversationTitleView.swift | 89 ---------- .../ConversationViewController.m | 99 +++++++++++ SignalServiceKit/src/Loki/API/LokiAPI.swift | 8 +- .../Loki/Messaging/Notification+Loki.swift | 10 ++ .../src/Messages/OWSMessageSender.m | 4 +- 8 files changed, 286 insertions(+), 94 deletions(-) create mode 100644 Signal/src/Loki/Redesign/Components/ConversationTitleView.swift delete mode 100644 Signal/src/Loki/Redesign/View Controllers/ConversationTitleView.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 388fa90e9..a9c733ed5 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -2776,6 +2776,7 @@ children = ( B8B5BCEB2394D869003823C9 /* Button.swift */, B8BB82AA238F669C00BA5194 /* ConversationCell.swift */, + B82B4093239DF15900A248E7 /* ConversationTitleView.swift */, B82B40892399EC0600A248E7 /* FakeChatView.swift */, B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */, B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */, @@ -2801,7 +2802,6 @@ B8CCF63D2397580E0091D419 /* View Controllers */ = { isa = PBXGroup; children = ( - B82B4093239DF15900A248E7 /* ConversationTitleView.swift */, B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */, B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */, B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */, diff --git a/Signal/src/Loki/Redesign/Components/ConversationTitleView.swift b/Signal/src/Loki/Redesign/Components/ConversationTitleView.swift new file mode 100644 index 000000000..141115b12 --- /dev/null +++ b/Signal/src/Loki/Redesign/Components/ConversationTitleView.swift @@ -0,0 +1,167 @@ + +@objc final class ConversationTitleView : UIView { + private let thread: TSThread + private var currentStatus: Status? { didSet { updateSubtitleForCurrentStatus() } } + + // MARK: Types + private enum Status : Int { + case calculatingPoW = 1 + case contactingNetwork = 2 + case sendingMessage = 3 + case messageSent = 4 + case messageFailed = 5 + } + + // 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: Lifecycle + @objc init(thread: TSThread) { + self.thread = thread + super.init(frame: CGRect.zero) + setUpViewHierarchy() + updateTitle() + 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(handleContactingNetworkNotification(_:)), name: .contactingNetwork, object: nil) + notificationCenter.addObserver(self, selector: #selector(handleSendingMessageNotification(_:)), name: .sendingMessage, object: nil) + notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil) + notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil) + } + + override init(frame: CGRect) { + preconditionFailure("Use init(thread:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(thread:) instead.") + } + + private func setUpViewHierarchy() { + let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ]) + stackView.axis = .vertical + stackView.alignment = .center + stackView.layoutMargins = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 0) // Compensate for settings button trailing margin + stackView.isLayoutMarginsRelativeArrangement = true + addSubview(stackView) + stackView.pin(to: self) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Updating + private func updateTitle() { + let title: String + if thread.isGroupThread() { + if thread.name().isEmpty { + title = NSLocalizedString("New Group", comment: "") + } else { + title = thread.name() + } + } else { + if thread.isNoteToSelf() { + title = NSLocalizedString("Note to Self", comment: "") + } else { + let hexEncodedPublicKey = thread.contactIdentifier()! + title = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey + } + } + titleLabel.text = title + } + + @objc private func handleProfileChangedNotification(_ notification: Notification) { + guard let hexEncodedPublicKey = notification.userInfo?[kNSNotificationKey_ProfileRecipientId] as? String, let thread = self.thread as? TSContactThread, + hexEncodedPublicKey == thread.contactIdentifier() else { return } + updateTitle() + } + + @objc private func handleCalculatingPoWNotification(_ notification: Notification) { + guard let timestamp = notification.object as? NSNumber else { return } + setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp) + } + + @objc private func handleContactingNetworkNotification(_ notification: Notification) { + guard let timestamp = notification.object as? NSNumber else { return } + setStatusIfNeeded(to: .contactingNetwork, forMessageWithTimestamp: timestamp) + } + + @objc private func handleSendingMessageNotification(_ notification: Notification) { + guard let timestamp = notification.object as? NSNumber else { return } + setStatusIfNeeded(to: .sendingMessage, forMessageWithTimestamp: timestamp) + } + + @objc private func handleMessageSentNotification(_ notification: Notification) { + guard let timestamp = notification.object as? NSNumber else { return } + setStatusIfNeeded(to: .messageSent, forMessageWithTimestamp: timestamp) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.clearStatusIfNeededForMessageWithTimestamp(timestamp) + } + } + + @objc private func handleMessageFailedNotification(_ notification: Notification) { + guard let timestamp = notification.object as? NSNumber else { return } + clearStatusIfNeededForMessageWithTimestamp(timestamp) + } + + private func setStatusIfNeeded(to status: Status, forMessageWithTimestamp timestamp: NSNumber) { + var uncheckedTargetInteraction: TSInteraction? = nil + thread.enumerateInteractions { interaction in + guard interaction.timestamp == timestamp.uint64Value else { return } + uncheckedTargetInteraction = interaction + } + guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage, status.rawValue > (currentStatus?.rawValue ?? 0) else { return } + currentStatus = status + } + + private func clearStatusIfNeededForMessageWithTimestamp(_ timestamp: NSNumber) { + var uncheckedTargetInteraction: TSInteraction? = nil + thread.enumerateInteractions { interaction in + guard interaction.timestamp == timestamp.uint64Value else { return } + uncheckedTargetInteraction = interaction + } + guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage else { return } + self.currentStatus = nil + } + + private func updateSubtitleForCurrentStatus() { + DispatchQueue.main.async { + switch self.currentStatus { + case .calculatingPoW: self.subtitleLabel.text = "Calculating proof of work" + case .contactingNetwork: self.subtitleLabel.text = "Contacting service node network" + case .sendingMessage: self.subtitleLabel.text = "Sending message" + case .messageSent: self.subtitleLabel.text = "Message sent securely" + case .messageFailed: self.subtitleLabel.text = "Message failed to send" + case nil: + let subtitle = NSMutableAttributedString() + if self.thread.isMuted { + subtitle.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ])) + } + subtitle.append(NSAttributedString(string: "26 members")) // TODO: Implement + self.subtitleLabel.attributedText = subtitle + } + } + } + + // MARK: Layout + public override var intrinsicContentSize: CGSize { + return UIView.layoutFittingExpandedSize + } +} diff --git a/Signal/src/Loki/Redesign/Style Guide/Values.swift b/Signal/src/Loki/Redesign/Style Guide/Values.swift index a9b037450..81a36f3e7 100644 --- a/Signal/src/Loki/Redesign/Style Guide/Values.swift +++ b/Signal/src/Loki/Redesign/Style Guide/Values.swift @@ -43,6 +43,7 @@ final class Values : NSObject { @objc static let fakeChatViewHeight = CGFloat(234) @objc static var composeViewTextFieldBorderThickness: CGFloat { return 1 / UIScreen.main.scale } @objc static let messageBubbleCornerRadius: CGFloat = 10 + @objc static let messageProgressBarThickness: CGFloat = 2 // MARK: - Distances @objc static let verySmallSpacing = CGFloat(4) diff --git a/Signal/src/Loki/Redesign/View Controllers/ConversationTitleView.swift b/Signal/src/Loki/Redesign/View Controllers/ConversationTitleView.swift deleted file mode 100644 index 7571d0f4c..000000000 --- a/Signal/src/Loki/Redesign/View Controllers/ConversationTitleView.swift +++ /dev/null @@ -1,89 +0,0 @@ - -@objc final class ConversationTitleView : UIView { - private let thread: TSThread - - // 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: Lifecycle - @objc init(thread: TSThread) { - self.thread = thread - super.init(frame: CGRect.zero) - setUpViewHierarchy() - update() - NotificationCenter.default.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil) - } - - override init(frame: CGRect) { - preconditionFailure("Use init(thread:) instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(thread:) instead.") - } - - private func setUpViewHierarchy() { - let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ]) - stackView.axis = .vertical - stackView.alignment = .center - stackView.layoutMargins = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 0) // Compensate for settings button trailing margin - stackView.isLayoutMarginsRelativeArrangement = true - addSubview(stackView) - stackView.pin(to: self) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: Updating - private func update() { - let title: String - if thread.isGroupThread() { - if thread.name().isEmpty { - title = NSLocalizedString("New Group", comment: "") - } else { - title = thread.name() - } - } else { - if thread.isNoteToSelf() { - title = NSLocalizedString("Note to Self", comment: "") - } else { - let hexEncodedPublicKey = thread.contactIdentifier()! - title = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey - } - } - titleLabel.text = title - let subtitle = NSMutableAttributedString() - if thread.isMuted { - subtitle.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ])) - } - subtitle.append(NSAttributedString(string: "26 members")) // TODO: Implement - subtitleLabel.attributedText = subtitle - } - - @objc private func handleProfileChangedNotification(_ notification: Notification) { - guard let hexEncodedPublicKey = notification.userInfo?[kNSNotificationKey_ProfileRecipientId] as? String, let thread = self.thread as? TSContactThread, - hexEncodedPublicKey == thread.contactIdentifier() else { return } - update() - } - - // MARK: Layout - public override var intrinsicContentSize: CGSize { - return UIView.layoutFittingExpandedSize - } -} diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 11052692a..221a1e3af 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -150,6 +150,7 @@ typedef enum : NSUInteger { @property (nonatomic, readonly) ConversationInputToolbar *inputToolbar; @property (nonatomic, readonly) ConversationCollectionView *collectionView; +@property (nonatomic, readonly) UIProgressView *progressIndicatorView; @property (nonatomic, readonly) ConversationViewLayout *layout; @property (nonatomic, readonly) ConversationStyle *conversationStyle; @@ -417,6 +418,26 @@ typedef enum : NSUInteger { selector:@selector(handleThreadFriendRequestStatusChangedNotification:) name:NSNotification.threadFriendRequestStatusChanged object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleCalculatingPoWNotification:) + name:NSNotification.calculatingPoW + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleContactingNetworkNotification:) + name:NSNotification.contactingNetwork + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSendingMessageNotification:) + name:NSNotification.sendingMessage + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleMessageSentNotification:) + name:NSNotification.messageSent + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleMessageFailedNotification:) + name:NSNotification.messageFailed + object:nil]; } - (BOOL)isGroupConversation @@ -675,6 +696,17 @@ typedef enum : NSUInteger { [self.collectionView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading]; [self.collectionView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing]; + _progressIndicatorView = [UIProgressView new]; + [self.progressIndicatorView autoSetDimension:ALDimensionHeight toSize:LKValues.messageProgressBarThickness]; + self.progressIndicatorView.progressViewStyle = UIProgressViewStyleBar; + self.progressIndicatorView.progressTintColor = LKColors.accent; + self.progressIndicatorView.trackTintColor = UIColor.clearColor; + self.progressIndicatorView.alpha = 0; + [self.view addSubview:self.progressIndicatorView]; + [self.progressIndicatorView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [self.progressIndicatorView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading]; + [self.progressIndicatorView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing]; + [self.collectionView applyScrollViewInsetsFix]; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _collectionView); @@ -5243,6 +5275,73 @@ typedef enum : NSUInteger { [self updateScrollDownButtonLayout]; } +- (void)handleCalculatingPoWNotification:(NSNotification *)notification +{ + NSNumber *timestamp = (NSNumber *)notification.object; + [self setProgressIfNeededTo:0.25f forMessageWithTimestamp:timestamp]; +} + +- (void)handleContactingNetworkNotification:(NSNotification *)notification +{ + NSNumber *timestamp = (NSNumber *)notification.object; + [self setProgressIfNeededTo:0.50f forMessageWithTimestamp:timestamp]; +} + +- (void)handleSendingMessageNotification:(NSNotification *)notification +{ + NSNumber *timestamp = (NSNumber *)notification.object; + [self setProgressIfNeededTo:0.75f forMessageWithTimestamp:timestamp]; +} + +- (void)handleMessageSentNotification:(NSNotification *)notification +{ + NSNumber *timestamp = (NSNumber *)notification.object; + [self setProgressIfNeededTo:1.0f forMessageWithTimestamp:timestamp]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void) { + [self hideProgressIndicatorViewForMessageWithTimestamp:timestamp]; + }); +} + +- (void)handleMessageFailedNotification:(NSNotification *)notification +{ + NSNumber *timestamp = (NSNumber *)notification.object; + [self hideProgressIndicatorViewForMessageWithTimestamp:timestamp]; +} + +- (void)setProgressIfNeededTo:(float)progress forMessageWithTimestamp:(NSNumber *)timestamp +{ + __block TSInteraction *targetInteraction; + [self.thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { + if (interaction.timestamp == timestamp.unsignedLongLongValue) { + targetInteraction = interaction; + } + }]; + if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + if (progress <= self.progressIndicatorView.progress) { return; } + self.progressIndicatorView.alpha = 1; + [self.progressIndicatorView setProgress:progress animated:YES]; + }); +} + +- (void)hideProgressIndicatorViewForMessageWithTimestamp:(NSNumber *)timestamp +{ + __block TSInteraction *targetInteraction; + [self.thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { + if (interaction.timestamp == timestamp.unsignedLongLongValue) { + targetInteraction = interaction; + } + }]; + if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.25 animations:^{ + self.progressIndicatorView.alpha = 0; + } completion:^(BOOL finished) { + [self.progressIndicatorView setProgress:0.0f]; + }]; + }); +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index e192f8915..2fb7e325c 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -151,16 +151,20 @@ public final class LokiAPI : NSObject { public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise> { guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: Error.messageConversionFailed) } + let notificationCenter = NotificationCenter.default let destination = lokiMessage.destination func sendLokiMessage(_ lokiMessage: LokiMessage, to target: LokiAPITarget) -> RawResponsePromise { let parameters = lokiMessage.toJSON() return invoke(.sendMessage, on: target, associatedWith: destination, parameters: parameters) } func sendLokiMessageUsingSwarmAPI() -> Promise> { - return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW in + notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp)) + return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW -> Promise> in + notificationCenter.post(name: .contactingNetwork, object: NSNumber(value: signalMessage.timestamp)) return getTargetSnodes(for: destination).map { swarm in return Set(swarm.map { target in - sendLokiMessage(lokiMessageWithPoW, to: target).map { rawResponse in + notificationCenter.post(name: .sendingMessage, object: NSNumber(value: signalMessage.timestamp)) + return sendLokiMessage(lokiMessageWithPoW, to: target).map { rawResponse in if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int { guard powDifficulty != LokiAPI.powDifficulty else { return rawResponse } print("[Loki] Setting proof of work difficulty to \(powDifficulty).") diff --git a/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift b/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift index 061c3144f..92990a9ab 100644 --- a/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift +++ b/SignalServiceKit/src/Loki/Messaging/Notification+Loki.swift @@ -6,6 +6,11 @@ public extension Notification.Name { public static let messageFriendRequestStatusChanged = Notification.Name("messageFriendRequestStatusChanged") public static let threadDeleted = Notification.Name("threadDeleted") public static let dataNukeRequested = Notification.Name("dataNukeRequested") + public static let calculatingPoW = Notification.Name("calculatingPoW") + public static let contactingNetwork = Notification.Name("contactingNetwork") + public static let sendingMessage = Notification.Name("sendingMessage") + public static let messageSent = Notification.Name("messageSent") + public static let messageFailed = Notification.Name("messageFailed") } @objc public extension NSNotification { @@ -15,4 +20,9 @@ public extension Notification.Name { @objc public static let messageFriendRequestStatusChanged = Notification.Name.messageFriendRequestStatusChanged.rawValue as NSString @objc public static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString + @objc public static let calculatingPoW = Notification.Name.calculatingPoW.rawValue as NSString + @objc public static let contactingNetwork = Notification.Name.contactingNetwork.rawValue as NSString + @objc public static let sendingMessage = Notification.Name.sendingMessage.rawValue as NSString + @objc public static let messageSent = Notification.Name.messageSent.rawValue as NSString + @objc public static let messageFailed = Notification.Name.messageFailed.rawValue as NSString } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index f483239ef..2a5d3d881 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1308,7 +1308,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [promise .thenOn(OWSDispatch.sendingQueue, ^(id result) { if (isSuccess) { return; } // Succeed as soon as the first promise succeeds - [LKAnalytics.shared track:@"Sent Message Using Swarm API"]; + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageSent object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]]; isSuccess = YES; if (signalMessage.type == TSFriendRequestMessageType) { if (!message.skipSave) { @@ -1330,7 +1330,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { errorCount += 1; if (errorCount != promiseCount) { return; } // Only error out if all promises failed - [LKAnalytics.shared track:@"Failed to Send Message Using Swarm API"]; + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageFailed object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]]; handleError(error); }) retainUntilComplete]; }