Implement the much anticipated Simon status bar

pull/68/head
Niels Andriesse 5 years ago
parent 7c5adb95d3
commit d05df87dd2

@ -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 */,

@ -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
}
}

@ -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)

@ -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
}
}

@ -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

@ -151,16 +151,20 @@ public final class LokiAPI : NSObject {
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
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<Set<RawResponsePromise>> {
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<Set<RawResponsePromise>> 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).")

@ -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
}

@ -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];
}

Loading…
Cancel
Save