From 3f8554459404d69fd10b01668243ebeac1e80df2 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 10 Feb 2021 14:43:57 +1100 Subject: [PATCH] Finish replies UI --- .../ConversationVC+Interaction.swift | 9 +- .../Input View/InputView.swift | 24 ++- .../Content Views/QuoteView.swift | 137 ++++++++++++------ .../Message Cells/VisibleMessageCell.swift | 8 +- Session/Conversations/ConversationViewModel.m | 2 +- Session/Meta/AppDelegate.m | 6 +- .../Quotes/OWSQuotedReplyModel.h | 4 +- 7 files changed, 135 insertions(+), 55 deletions(-) diff --git a/Session/Conversations V2/ConversationVC+Interaction.swift b/Session/Conversations V2/ConversationVC+Interaction.swift index c9e7afb8b..aa2d392d0 100644 --- a/Session/Conversations V2/ConversationVC+Interaction.swift +++ b/Session/Conversations V2/ConversationVC+Interaction.swift @@ -148,7 +148,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func reply(_ viewItem: ConversationViewItem) { - // TODO: Implement + var quoteDraftOrNil: OWSQuotedReplyModel? + Storage.read { transaction in + quoteDraftOrNil = OWSQuotedReplyModel.quotedReplyForSending(with: viewItem, threadId: viewItem.interaction.uniqueThreadId, transaction: transaction) + } + guard let quoteDraft = quoteDraftOrNil else { return } + let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage) + snInputView.quoteDraftInfo = (model: quoteDraft, isOutgoing: isOutgoing) + snInputView.becomeFirstResponder() } func copy(_ viewItem: ConversationViewItem) { diff --git a/Session/Conversations V2/Input View/InputView.swift b/Session/Conversations V2/Input View/InputView.swift index 787fe9f55..30f1ef6ef 100644 --- a/Session/Conversations V2/Input View/InputView.swift +++ b/Session/Conversations V2/Input View/InputView.swift @@ -1,6 +1,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate { private let delegate: InputViewDelegate + var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } } var text: String { get { inputTextView.text } @@ -17,6 +18,12 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate { private lazy var sendButton = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self) private lazy var inputTextView = InputTextView(delegate: self) + + private lazy var quoteDraftContainer: UIView = { + let result = UIView() + result.heightAnchor.constraint(greaterThanOrEqualToConstant: 12).isActive = true + return result + }() // MARK: Lifecycle init(delegate: InputViewDelegate) { @@ -68,9 +75,8 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate { bottomStackView.axis = .horizontal bottomStackView.spacing = Values.smallSpacing // Main stack view - let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, bottomStackView ]) + let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, quoteDraftContainer, bottomStackView ]) mainStackView.axis = .vertical - mainStackView.spacing = 12 mainStackView.isLayoutMarginsRelativeArrangement = true let adjustment = (InputViewButton.expandedSize - InputViewButton.size) / 2 mainStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.largeSpacing, bottom: Values.smallSpacing, trailing: Values.largeSpacing - adjustment) @@ -84,6 +90,20 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate { func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() } + + private func handleQuoteDraftChanged() { + quoteDraftContainer.subviews.forEach { $0.removeFromSuperview() } + guard let quoteDraftInfo = quoteDraftInfo else { return } + let direction: QuoteView.Direction = quoteDraftInfo.isOutgoing ? .outgoing : .incoming + let hInset: CGFloat = 6 + let maxMessageWidth = quoteDraftContainer.bounds.width - 2 * hInset + let quoteView = QuoteView(for: quoteDraftInfo.model, direction: direction, hInset: hInset, maxMessageWidth: maxMessageWidth) + quoteDraftContainer.addSubview(quoteView) + quoteView.pin(.left, to: .left, of: quoteDraftContainer, withInset: hInset) + quoteView.pin(.top, to: .top, of: quoteDraftContainer, withInset: 12) + quoteView.pin(.right, to: .right, of: quoteDraftContainer, withInset: -hInset) + quoteView.pin(.bottom, to: .bottom, of: quoteDraftContainer, withInset: -12) + } // MARK: Interaction func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) { diff --git a/Session/Conversations V2/Message Cells/Content Views/QuoteView.swift b/Session/Conversations V2/Message Cells/Content Views/QuoteView.swift index 5eeacd7a7..2762acf79 100644 --- a/Session/Conversations V2/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations V2/Message Cells/Content Views/QuoteView.swift @@ -1,48 +1,101 @@ final class QuoteView : UIView { - private let viewItem: ConversationViewItem + private let mode: Mode + private let direction: Direction + private let hInset: CGFloat private let maxMessageWidth: CGFloat - private var direction: Direction { - guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() } - switch message { - case is TSIncomingMessage: return .incoming - case is TSOutgoingMessage: return .outgoing - default: preconditionFailure() + private var attachments: [OWSAttachmentInfo] { + switch mode { + case .regular(let viewItem): return (viewItem.interaction as? TSMessage)?.quotedMessage!.quotedAttachments ?? [] + case .draft(let model): return given(model.attachmentStream) { [ OWSAttachmentInfo(attachmentStream: $0) ] } ?? [] + } + } + + private var thumbnail: UIImage? { + switch mode { + case .regular(let viewItem): return viewItem.quotedReply!.thumbnailImage + case .draft(let model): return model.thumbnailImage + } + } + + private var body: String? { + switch mode { + case .regular(let viewItem): return (viewItem.interaction as? TSMessage)?.quotedMessage!.body + case .draft(let model): return model.body + } + } + + private var threadID: String { + switch mode { + case .regular(let viewItem): return viewItem.interaction.uniqueThreadId + case .draft(let model): return model.threadId + } + } + + private var isGroupThread: Bool { + switch mode { + case .regular(let viewItem): return viewItem.isGroupThread + case .draft(let model): + var result = false + Storage.read { transaction in + result = TSThread.fetch(uniqueId: model.threadId, transaction: transaction)?.isGroupThread() ?? false + } + return result + } + } + + private var authorID: String { + switch mode { + case .regular(let viewItem): return viewItem.quotedReply!.authorId + case .draft(let model): return model.authorId } } private var lineColor: UIColor { - return .black + switch (mode, AppModeManager.shared.currentAppMode) { + case (.regular, _), (.draft, .light): return .black + case (.draft, .dark): return Colors.accent + } } private var textColor: UIColor { + if case .draft = mode { return Colors.text } switch (direction, AppModeManager.shared.currentAppMode) { - case (.outgoing, .dark), (.incoming, .light): return .white - default: return .black + case (.outgoing, .dark), (.incoming, .light): return .black + default: return .white } } - private var snBackgroundColor: UIColor { - switch direction { - case .outgoing: return Colors.receivedMessageBackground - case .incoming: return Colors.sentMessageBackground - } + // MARK: Mode + enum Mode { + case regular(ConversationViewItem) + case draft(OWSQuotedReplyModel) } // MARK: Direction enum Direction { case incoming, outgoing } // MARK: Settings - static let inset = Values.smallSpacing static let thumbnailSize: CGFloat = 48 static let iconSize: CGFloat = 24 static let labelStackViewSpacing: CGFloat = 2 // MARK: Lifecycle - init(for viewItem: ConversationViewItem, maxMessageWidth: CGFloat) { - self.viewItem = viewItem + init(for viewItem: ConversationViewItem, direction: Direction, hInset: CGFloat, maxMessageWidth: CGFloat) { + self.mode = .regular(viewItem) + self.maxMessageWidth = maxMessageWidth + self.direction = direction + self.hInset = hInset + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + init(for model: OWSQuotedReplyModel, direction: Direction, hInset: CGFloat, maxMessageWidth: CGFloat) { + self.mode = .draft(model) self.maxMessageWidth = maxMessageWidth + self.direction = direction + self.hInset = hInset super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -56,21 +109,19 @@ final class QuoteView : UIView { } private func setUpViewHierarchy() { - guard let quote = (viewItem.interaction as? TSMessage)?.quotedMessage else { return } - let hasAttachments = !quote.quotedAttachments.isEmpty + let hasAttachments = !attachments.isEmpty let thumbnailSize = QuoteView.thumbnailSize let iconSize = QuoteView.iconSize let labelStackViewSpacing = QuoteView.labelStackViewSpacing let smallSpacing = Values.smallSpacing - let inset = QuoteView.inset let availableWidth: CGFloat if !hasAttachments { - availableWidth = maxMessageWidth - 2 * inset - Values.accentLineThickness - 2 * smallSpacing + availableWidth = maxMessageWidth - 2 * hInset - Values.accentLineThickness - 2 * smallSpacing } else { - availableWidth = maxMessageWidth - 2 * inset - thumbnailSize - 2 * smallSpacing + availableWidth = maxMessageWidth - 2 * hInset - thumbnailSize - 2 * smallSpacing } let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude) - var body = quote.body + var body = self.body // Main stack view let mainStackView = UIStackView(arrangedSubviews: []) mainStackView.axis = .horizontal @@ -80,11 +131,9 @@ final class QuoteView : UIView { mainStackView.alignment = .center // Content view let contentView = UIView() - contentView.backgroundColor = snBackgroundColor - contentView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius - contentView.layer.masksToBounds = true addSubview(contentView) - contentView.pin(to: self, withInset: inset) + contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self) + contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true // Line view let lineView = UIView() lineView.backgroundColor = lineColor @@ -92,21 +141,26 @@ final class QuoteView : UIView { if !hasAttachments { mainStackView.addArrangedSubview(lineView) } else { - let image = viewItem.quotedReply?.thumbnailImage - let fallbackImage = UIImage(named: "actionsheet_document_black")?.withTint(.white)?.resizedImage(to: CGSize(width: iconSize, height: iconSize)) - let imageView = UIImageView(image: image ?? fallbackImage) - imageView.contentMode = (image != nil) ? .scaleAspectFill : .center + let isAudio = MIMETypeUtil.isAudio(attachments.first!.contentType!) + let fallbackImageName = isAudio ? "attachment_audio" : "actionsheet_document_black" + let fallbackImage = UIImage(named: fallbackImageName)?.withTint(.white)?.resizedImage(to: CGSize(width: iconSize, height: iconSize)) + let imageView = UIImageView(image: thumbnail ?? fallbackImage) + imageView.contentMode = (thumbnail != nil) ? .scaleAspectFill : .center imageView.backgroundColor = lineColor + imageView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius + imageView.layer.masksToBounds = true imageView.set(.width, to: thumbnailSize) imageView.set(.height, to: thumbnailSize) mainStackView.addArrangedSubview(imageView) - body = (image != nil) ? "Image" : "Document" + body = (thumbnail != nil) ? "Image" : (isAudio ? "Audio" : "Document") } // Body label let bodyLabel = UILabel() bodyLabel.numberOfLines = 0 bodyLabel.lineBreakMode = .byTruncatingTail - bodyLabel.text = given(body) { MentionUtilities.highlightMentions(in: $0, threadID: viewItem.interaction.uniqueThreadId) } ?? "Document" + let isOutgoing = (direction == .outgoing) + bodyLabel.attributedText = given(body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: threadID, attributes: [:]) } + ?? given(attachments.first?.contentType) { NSAttributedString(string: MIMETypeUtil.isAudio($0) ? "Audio" : "Document") } ?? NSAttributedString(string: "Document") bodyLabel.textColor = textColor bodyLabel.font = .systemFont(ofSize: Values.smallFontSize) if hasAttachments { @@ -114,10 +168,10 @@ final class QuoteView : UIView { } let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace) // Label stack view - if viewItem.isGroupThread { + if isGroupThread { let authorLabel = UILabel() authorLabel.lineBreakMode = .byTruncatingTail - authorLabel.text = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: quote.authorId, avoidingWriteTransaction: true) + authorLabel.text = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: authorID, avoidingWriteTransaction: true) authorLabel.textColor = textColor authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace) @@ -132,19 +186,20 @@ final class QuoteView : UIView { // Constraints contentView.addSubview(mainStackView) mainStackView.pin(to: contentView) - if !viewItem.isGroupThread { + if !isGroupThread { bodyLabel.set(.width, to: bodyLabelSize.width) } - let bodyLabelHeight = bodyLabelSize.height + let maxBodyLabelHeight: CGFloat = 72 + let bodyLabelHeight = bodyLabelSize.height.clamp(0, maxBodyLabelHeight) let authorLabelHeight: CGFloat = 14.33 - let isAuthorShown = viewItem.isGroupThread + let isAuthorShown = isGroupThread let contentViewHeight: CGFloat if hasAttachments { - contentViewHeight = thumbnailSize + contentViewHeight = thumbnailSize + 8 } else { contentViewHeight = bodyLabelHeight + 2 * smallSpacing + (isAuthorShown ? (authorLabelHeight + labelStackViewSpacing) : 0) } contentView.set(.height, to: contentViewHeight) - lineView.set(.height, to: contentViewHeight) + lineView.set(.height, to: contentViewHeight - 8) } } diff --git a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift index ec145ac4b..0e159aaa0 100644 --- a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift @@ -221,11 +221,15 @@ final class VisibleMessageCell : MessageCell { // Stack view let stackView = UIStackView(arrangedSubviews: []) stackView.axis = .vertical + stackView.spacing = 2 // Quote label if viewItem.quotedReply != nil { let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - let quoteView = QuoteView(for: viewItem, maxMessageWidth: maxMessageWidth) - stackView.addArrangedSubview(quoteView) + let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming + let hInset: CGFloat = 2 + let quoteView = QuoteView(for: viewItem, direction: direction, hInset: hInset, maxMessageWidth: maxMessageWidth) + let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset)) + stackView.addArrangedSubview(quoteViewContainer) } // Body label let bodyLabel = UILabel() diff --git a/Session/Conversations/ConversationViewModel.m b/Session/Conversations/ConversationViewModel.m index abf3aa0b1..880f90e39 100644 --- a/Session/Conversations/ConversationViewModel.m +++ b/Session/Conversations/ConversationViewModel.m @@ -171,7 +171,7 @@ NS_ASSUME_NONNULL_BEGIN // // PERF: we could do less messages on shorter (older, slower) devices // PERF: we could cache the cell height, since some messages will be much taller. -static const int kYapDatabasePageSize = 64; +static const int kYapDatabasePageSize = 24; // Never show more than n messages in conversation view when user arrives. static const int kConversationInitialMaxRangeSize = 300; diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 4e063823e..c558830ce 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -763,11 +763,7 @@ static NSTimeInterval launchStartedAt; - (LKAppMode)getCurrentAppMode { - UIWindow *window = UIApplication.sharedApplication.keyWindow; - if (window == nil) { return LKAppModeLight; } - UIUserInterfaceStyle userInterfaceStyle = window.traitCollection.userInterfaceStyle; - BOOL isLightMode = userInterfaceStyle == UIUserInterfaceStyleLight || userInterfaceStyle == UIUserInterfaceStyleUnspecified; - return isLightMode ? LKAppModeLight : LKAppModeDark; + return [NSUserDefaults.standardUserDefaults integerForKey:@"appMode"]; } - (void)setCurrentAppMode:(LKAppMode)appMode diff --git a/SessionMessagingKit/Sending & Receiving/Quotes/OWSQuotedReplyModel.h b/SessionMessagingKit/Sending & Receiving/Quotes/OWSQuotedReplyModel.h index 44dc4be9c..7425533fa 100644 --- a/SessionMessagingKit/Sending & Receiving/Quotes/OWSQuotedReplyModel.h +++ b/SessionMessagingKit/Sending & Receiving/Quotes/OWSQuotedReplyModel.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, readonly, nullable) TSAttachmentPointer *thumbnailAttachmentPointer; @property (nonatomic, readonly) BOOL thumbnailDownloadFailed; +@property (nonatomic, readonly) NSString *threadId; // This property should be set IFF we are quoting a text message // or attachment with caption. @@ -51,9 +52,6 @@ NS_ASSUME_NONNULL_BEGIN - (TSQuotedMessage *)buildQuotedMessageForSending; -// Loki -@property (nonatomic, readonly) NSString *threadId; - @end NS_ASSUME_NONNULL_END