From 50381cc94cfb503e5715ba882aa28ae437db98dc Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 1 Nov 2018 10:43:13 -0400 Subject: [PATCH] Add typing indicators in home view. --- Signal.xcodeproj/project.pbxproj | 8 +- .../ViewControllers/HomeView/HomeViewCell.m | 59 +++++++++- Signal/src/views/TypingIndicatorView.swift | 101 ++++++++++++++++++ .../src/Util/TypingIndicators.swift | 7 +- 4 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 Signal/src/views/TypingIndicatorView.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 39439a41d..5ecf813a3 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */; }; 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */; }; 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */; }; + 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */; }; 34B6D27420F664C900765BE2 /* OWSUnreadIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34B6D27520F664C900765BE2 /* OWSUnreadIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B6D27320F664C800765BE2 /* OWSUnreadIndicator.m */; }; 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2A1F74C12700D7438D /* DebugUIStress.m */; }; @@ -866,6 +867,7 @@ 34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewGroupViewController.m; sourceTree = ""; }; 34B3F86D1E8DF1700035BE1A /* SignalsNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalsNavigationController.h; sourceTree = ""; }; 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalsNavigationController.m; sourceTree = ""; }; + 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicator.h; sourceTree = ""; }; 34B6D27320F664C800765BE2 /* OWSUnreadIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicator.m; sourceTree = ""; }; 34BECE291F74C12700D7438D /* DebugUIStress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIStress.h; sourceTree = ""; }; @@ -2215,10 +2217,11 @@ isa = PBXGroup; children = ( 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */, - 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */, 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */, + 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */, 451764291DE939FD00EDB8B9 /* ContactCell.swift */, 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */, + 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */, 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */, 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */, 34386A53207D271C009F5D9C /* NeverClearView.swift */, @@ -2234,8 +2237,8 @@ 45A6DAD51EBBF85500893231 /* ReminderView.swift */, 450D19111F85236600970622 /* RemoteVideoView.h */, 450D19121F85236600970622 /* RemoteVideoView.m */, - 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */, 4CA5F792211E1F06008C2708 /* Toast.swift */, + 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, ); name = Views; path = views; @@ -3382,6 +3385,7 @@ 457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */, 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */, 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */, + 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */, 340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index 224575dc6..45691640c 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UILabel *snippetLabel; @property (nonatomic) UILabel *dateTimeLabel; @property (nonatomic) MessageStatusView *messageStatusView; +@property (nonatomic) TypingIndicatorView *typingIndicatorView; +@property (nonatomic) UIStackView *previewStackView; @property (nonatomic) UIView *unreadBadge; @property (nonatomic) UILabel *unreadLabel; @@ -45,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN return Environment.shared.contactsManager; } +- (id)typingIndicators +{ + return SSKEnvironment.shared.typingIndicators; +} + #pragma mark - - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier @@ -111,15 +118,25 @@ NS_ASSUME_NONNULL_BEGIN self.snippetLabel.font = [self snippetFont]; self.snippetLabel.numberOfLines = 1; self.snippetLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self.snippetLabel setContentHuggingHorizontalLow]; - [self.snippetLabel setCompressionResistanceHorizontalLow]; + + self.typingIndicatorView = [TypingIndicatorView new]; + + self.previewStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.snippetLabel, + self.typingIndicatorView, + ]]; + self.previewStackView.axis = UILayoutConstraintAxisVertical; + self.previewStackView.alignment = UIStackViewAlignmentLeading; + [self.previewStackView setContentHuggingHorizontalLow]; + [self.previewStackView setCompressionResistanceHorizontalLow]; UIStackView *bottomRowView = [[UIStackView alloc] initWithArrangedSubviews:@[ - self.snippetLabel, + self.previewStackView, self.messageStatusView, ]]; + bottomRowView.axis = UILayoutConstraintAxisHorizontal; - bottomRowView.alignment = UIStackViewAlignmentLastBaseline; + bottomRowView.alignment = UIStackViewAlignmentCenter; bottomRowView.spacing = 6.f; UIStackView *vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ @@ -203,6 +220,10 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(otherUsersProfileDidChange:) name:kNSNotificationName_OtherUsersProfileDidChange object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(typingIndicatorStateDidChange:) + name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange] + object:nil]; [self updateNameLabel]; [self updateAvatarView]; @@ -215,7 +236,13 @@ NS_ASSUME_NONNULL_BEGIN } else { self.snippetLabel.attributedText = [self attributedSnippetForThread:thread isBlocked:isBlocked]; } - + [self updatePreview]; + CGFloat previewHeight = MAX(self.snippetLabel.font.lineHeight, + TypingIndicatorView.kMaxRadiusPt); + [self.viewConstraints addObjectsFromArray:@[ + [self.previewStackView autoSetDimension:ALDimensionHeight + toSize:previewHeight], + ]]; self.dateTimeLabel.text = (overrideDate ? [self stringForDate:overrideDate] : [self stringForDate:thread.lastMessageDate]); @@ -500,6 +527,28 @@ NS_ASSUME_NONNULL_BEGIN self.nameLabel.attributedText = name; } +#pragma mark - Typing Indicators + +- (void)updatePreview +{ + if ([self.typingIndicators typingIndicatorsForThread:self.thread.threadRecord] != nil) { + self.snippetLabel.hidden = YES; + self.typingIndicatorView.hidden = NO; + [self.typingIndicatorView startAnimation]; + } else { + self.snippetLabel.hidden = NO; + self.typingIndicatorView.hidden = YES; + [self.typingIndicatorView stopAnimation]; + } +} + +- (void)typingIndicatorStateDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self updatePreview]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/views/TypingIndicatorView.swift b/Signal/src/views/TypingIndicatorView.swift new file mode 100644 index 000000000..446c2fb82 --- /dev/null +++ b/Signal/src/views/TypingIndicatorView.swift @@ -0,0 +1,101 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +@objc class TypingIndicatorView: UIStackView { + private let kDotMaxHSpacing: CGFloat = 8 + + @objc + public static let kMinRadiusPt: CGFloat = 6 + @objc + public static let kMaxRadiusPt: CGFloat = 8 + + private let dot1 = DotView(dotType: .dotType1) + private let dot2 = DotView(dotType: .dotType2) + private let dot3 = DotView(dotType: .dotType3) + + override public var isHidden: Bool { + didSet { + Logger.verbose("\(oldValue) -> \(isHidden)") + } + } + + @available(*, unavailable, message:"use other constructor instead.") + required init(coder aDecoder: NSCoder) { + notImplemented() + } + + @available(*, unavailable, message:"use other constructor instead.") + override init(frame: CGRect) { + notImplemented() + } + + @objc + public init() { + super.init(frame: .zero) + + // init(arrangedSubviews:...) is not a designated initializer. + addArrangedSubview(dot1) + addArrangedSubview(dot2) + addArrangedSubview(dot3) + + self.axis = .horizontal + self.spacing = kDotMaxHSpacing + self.alignment = .center + } + + @objc + public func startAnimation() { + } + + @objc + public func stopAnimation() { + } + + private enum DotType { + case dotType1 + case dotType2 + case dotType3 + } + + private class DotView: UIView { + private let dotType: DotType + + private let shapeLayer = CAShapeLayer() + + @available(*, unavailable, message:"use other constructor instead.") + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + @available(*, unavailable, message:"use other constructor instead.") + override init(frame: CGRect) { + notImplemented() + } + + init(dotType: DotType) { + self.dotType = dotType + + super.init(frame: .zero) + + autoSetDimension(.width, toSize: kMaxRadiusPt) + autoSetDimension(.height, toSize: kMaxRadiusPt) + + self.layer.addSublayer(shapeLayer) + + updateLayer() +// self.text = text +// +// setupSubviews() + } + + private func updateLayer() { + shapeLayer.fillColor = UIColor.ows_signalBlue.cgColor + + let margin = (TypingIndicatorView.kMaxRadiusPt - TypingIndicatorView.kMinRadiusPt) * 0.5 + let bezierPath = UIBezierPath(ovalIn: CGRect(x: margin, y: margin, width: TypingIndicatorView.kMinRadiusPt, height: TypingIndicatorView.kMinRadiusPt)) + shapeLayer.path = bezierPath.cgPath + + } + } +} diff --git a/SignalServiceKit/src/Util/TypingIndicators.swift b/SignalServiceKit/src/Util/TypingIndicators.swift index 5967a76c2..043a524e6 100644 --- a/SignalServiceKit/src/Util/TypingIndicators.swift +++ b/SignalServiceKit/src/Util/TypingIndicators.swift @@ -31,7 +31,7 @@ public protocol TypingIndicators: class { // // TODO: Use this method. @objc - func typingIndicators(forThread thread: TSThread, recipientId: String) -> String? + func typingIndicators(forThread thread: TSThread) -> String? @objc func setTypingIndicatorsEnabled(value: Bool) @@ -45,7 +45,8 @@ public protocol TypingIndicators: class { @objc(OWSTypingIndicatorsImpl) public class TypingIndicatorsImpl: NSObject, TypingIndicators { - @objc public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange") + @objc + public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange") private let kDatabaseCollection = "TypingIndicators" private let kDatabaseKey_TypingIndicatorsEnabled = "kDatabaseKey_TypingIndicatorsEnabled" @@ -150,7 +151,7 @@ public class TypingIndicatorsImpl: NSObject, TypingIndicators { } @objc - public func typingIndicators(forThread thread: TSThread, recipientId: String) -> String? { + public func typingIndicators(forThread thread: TSThread) -> String? { AssertIsOnMainThread() var firstRecipientId: String?