Finish replies UI

This commit is contained in:
nielsandriesse 2021-02-10 14:43:57 +11:00
parent 217e4dad38
commit 3f85544594
7 changed files with 135 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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