diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index 9936e6c14..09f5a49a7 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -53,6 +53,8 @@ typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) { - (CGFloat)minWidth; +- (CGFloat)minHeight; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index ce012b29d..14ee8e436 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -273,6 +273,11 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4; return (kOWSMessageCellCornerRadius_Large * 2); } +- (CGFloat)minHeight +{ + return (kOWSMessageCellCornerRadius_Large * 2); +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift b/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift index 5def3dceb..670d10261 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift @@ -10,28 +10,146 @@ public class TypingIndicatorCell: ConversationViewCell { @objc public static let cellReuseIdentifier = "TypingIndicatorCell" - @available(*, unavailable, message:"use other constructor instead.") + @available(*, unavailable, message:"use other constructor instead.") @objc public required init(coder aDecoder: NSCoder) { notImplemented() } + private let kAvatarSize: CGFloat = 36 + private let kAvatarHSpacing: CGFloat = 8 + + private let avatarView = AvatarImageView() + private let bubbleView = OWSBubbleView() + private let typingIndicatorView = TypingIndicatorView() + private var viewConstraints = [NSLayoutConstraint]() + override init(frame: CGRect) { super.init(frame: frame) + + commonInit() + } + + private func commonInit() { + self.layoutMargins = .zero + self.contentView.layoutMargins = .zero + + bubbleView.layoutMargins = .zero + + bubbleView.addSubview(typingIndicatorView) + contentView.addSubview(bubbleView) + + avatarView.autoSetDimension(.width, toSize: kAvatarSize) + avatarView.autoSetDimension(.height, toSize: kAvatarSize) + } + + deinit { + NotificationCenter.default.removeObserver(self) } @objc public override func loadForDisplay() { + guard let conversationStyle = self.conversationStyle else { + owsFailDebug("Missing conversationStyle") + return + } + bubbleView.bubbleColor = conversationStyle.bubbleColor(isIncoming: true) + typingIndicatorView.startAnimation() + typingIndicatorView.addBackgroundView(withBackgroundColor: UIColor.red) + + viewConstraints.append(contentsOf: [ + bubbleView.autoPinEdge(toSuperviewEdge: .leading, withInset: conversationStyle.gutterLeading), + bubbleView.autoPinEdge(toSuperviewEdge: .trailing, withInset: conversationStyle.gutterTrailing, relation: .greaterThanOrEqual), + bubbleView.autoPinTopToSuperviewMargin(withInset: 0), + bubbleView.autoPinBottomToSuperviewMargin(withInset: 0), + + typingIndicatorView.autoPinEdge(toSuperviewEdge: .leading, withInset: conversationStyle.textInsetHorizontal), + typingIndicatorView.autoPinEdge(toSuperviewEdge: .trailing, withInset: conversationStyle.textInsetHorizontal), + typingIndicatorView.autoPinTopToSuperviewMargin(withInset: conversationStyle.textInsetTop), + 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() + } + } + + private func configureAvatarView() -> UIView? { + guard let viewItem = self.viewItem else { + owsFailDebug("Missing viewItem") + return nil + } + guard let typingIndicators = viewItem.interaction as? TypingIndicatorInteraction else { + owsFailDebug("Missing typingIndicators") + return nil + } + guard shouldShowAvatar() else { + return nil + } + guard let colorName = viewItem.authorConversationColorName else { + owsFailDebug("Missing authorConversationColorName") + return nil + } + guard let authorAvatarImage = + OWSContactAvatarBuilder(signalId: typingIndicators.recipientId, + colorName: ConversationColorNameForString(colorName), + diameter: UInt(kAvatarSize)).build() else { + owsFailDebug("Could build avatar image") + return nil + } + avatarView.image = authorAvatarImage + return avatarView + } + + private func shouldShowAvatar() -> Bool { + guard let viewItem = self.viewItem else { + owsFailDebug("Missing viewItem") + return false + } + return viewItem.isGroupThread } @objc public override func cellSize() -> CGSize { - return .zero + guard let conversationStyle = self.conversationStyle else { + owsFailDebug("Missing conversationStyle") + return .zero + } + + let insetsSize = CGSize(width: conversationStyle.textInsetHorizontal * 2, + height: conversationStyle.textInsetTop + conversationStyle.textInsetBottom) + let typingIndicatorSize = typingIndicatorView.sizeThatFits(.zero) + let bubbleSize = CGSizeAdd(insetsSize, typingIndicatorSize) + + if shouldShowAvatar() { + let avatarSize = CGSize(width: kAvatarSize, height: kAvatarSize) + return CGSizeCeil(CGSize(width: avatarSize.width + kAvatarHSpacing + bubbleSize.width, + height: max(avatarSize.height, bubbleSize.height))) + } else { + return bubbleSize + } } @objc public override func prepareForReuse() { super.prepareForReuse() + + NSLayoutConstraint.deactivate(viewConstraints) + viewConstraints = [NSLayoutConstraint]() + + avatarView.image = nil + avatarView.removeFromSuperview() + + typingIndicatorView.stopAnimation() + + NotificationCenter.default.removeObserver(self) } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 6d533ece8..be6772231 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -143,14 +143,24 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) { OWSAssertDebug(transaction); - if (self.interaction.interactionType != OWSInteractionType_IncomingMessage) { - _authorConversationColorName = nil; - return; + 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; } - - TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction; - _authorConversationColorName = - [TSContactThread conversationColorNameForRecipientId:incomingMessage.authorId transaction:transaction]; } - (NSString *)itemId diff --git a/Signal/src/views/TypingIndicatorView.swift b/Signal/src/views/TypingIndicatorView.swift index 3fb4632eb..f78cd65df 100644 --- a/Signal/src/views/TypingIndicatorView.swift +++ b/Signal/src/views/TypingIndicatorView.swift @@ -16,12 +16,6 @@ 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() @@ -46,6 +40,11 @@ self.alignment = .center } + @objc + public override func sizeThatFits(_ size: CGSize) -> CGSize { + return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt) + } + @objc public func startAnimation() { } @@ -86,9 +85,6 @@ self.layer.addSublayer(shapeLayer) updateLayer() -// self.text = text -// -// setupSubviews() } private func updateLayer() { diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 3e1de19c2..81bdde795 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -193,6 +193,11 @@ CG_INLINE CGSize CGSizeScale(CGSize size, CGFloat factor) return CGSizeMake(size.width * factor, size.height * factor); } +CG_INLINE CGSize CGSizeAdd(CGSize left, CGSize right) +{ + return CGSizeMake(left.width + right.width, left.height + right.height); +} + CGFloat CGHairlineWidth(void); NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Contacts/TSThread.h b/SignalServiceKit/src/Contacts/TSThread.h index 098787a38..15cd2b20a 100644 --- a/SignalServiceKit/src/Contacts/TSThread.h +++ b/SignalServiceKit/src/Contacts/TSThread.h @@ -11,6 +11,9 @@ NS_ASSUME_NONNULL_BEGIN @class TSInvalidIdentityKeyReceivingErrorMessage; typedef NSString *ConversationColorName NS_STRING_ENUM; + +ConversationColorName ConversationColorNameForString(NSString *value); + extern ConversationColorName const ConversationColorNameCrimson; extern ConversationColorName const ConversationColorNameVermilion; extern ConversationColorName const ConversationColorNameBurlap; diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 4d046f132..aa7831e1c 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -19,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN +ConversationColorName ConversationColorNameForString(NSString *value) +{ + return value; +} + ConversationColorName const ConversationColorNameCrimson = @"red"; ConversationColorName const ConversationColorNameVermilion = @"orange"; ConversationColorName const ConversationColorNameBurlap = @"brown";