Merge pull request #647 from RyanRory/ui-media-message

Media message UI redesign
pull/690/head
RyanZhao 2 years ago committed by GitHub
commit b0eaeedc7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -667,7 +667,7 @@ extension ConversationVC:
.elements
.firstIndex(of: cellViewModel),
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell,
let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false),
let snapshot = cell.snContentView.snapshotView(afterScreenUpdates: false),
contextMenuWindow == nil,
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
for: cellViewModel,
@ -686,7 +686,7 @@ extension ConversationVC:
self.contextMenuWindow = ContextMenuWindow()
self.contextMenuVC = ContextMenuVC(
snapshot: snapshot,
frame: cell.convert(cell.bubbleView.frame, to: keyWindow),
frame: cell.convert(cell.snContentView.frame, to: keyWindow),
cellViewModel: cellViewModel,
actions: actions
) { [weak self] in

@ -8,8 +8,8 @@ public class MediaAlbumView: UIStackView {
public let itemViews: [MediaView]
public var moreItemsView: MediaView?
private static let kSpacingPts: CGFloat = 2
private static let kMaxItems = 5
private static let kSpacingPts: CGFloat = 4
private static let kMaxItems = 3
@available(*, unavailable, message: "use other init() instead.")
required public init(coder aDecoder: NSCoder) {
@ -35,9 +35,6 @@ public class MediaAlbumView: UIStackView {
super.init(frame: .zero)
// UIStackView's backgroundColor property has no effect.
addBackgroundView(withBackgroundColor: Colors.navigationBarBackground)
createContents(maxMessageWidth: maxMessageWidth)
}
@ -65,8 +62,8 @@ public class MediaAlbumView: UIStackView {
self.axis = .horizontal
self.distribution = .fillEqually
self.spacing = MediaAlbumView.kSpacingPts
case 3:
default:
// x
// X x
// Big on left, 2 small on right.
@ -90,64 +87,9 @@ public class MediaAlbumView: UIStackView {
)
self.axis = .horizontal
self.spacing = MediaAlbumView.kSpacingPts
case 4:
// X X
// X X
// Square
let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
let topViews = Array(itemViews[0..<2])
addArrangedSubview(
newRow(
rowViews: topViews,
axis: .horizontal,
viewSize: imageSize
)
)
let bottomViews = Array(itemViews[2..<4])
addArrangedSubview(
newRow(
rowViews: bottomViews,
axis: .horizontal,
viewSize: imageSize
)
)
self.axis = .vertical
self.spacing = MediaAlbumView.kSpacingPts
default:
// X X
// xxx
// 2 big on top, 3 small on bottom.
let bigImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3
let topViews = Array(itemViews[0..<2])
addArrangedSubview(
newRow(
rowViews: topViews,
axis: .horizontal,
viewSize: bigImageSize
)
)
let bottomViews = Array(itemViews[2..<5])
addArrangedSubview(
newRow(
rowViews: bottomViews,
axis: .horizontal,
viewSize: smallImageSize
)
)
self.axis = .vertical
self.spacing = MediaAlbumView.kSpacingPts
if items.count > MediaAlbumView.kMaxItems {
guard let lastView = bottomViews.last else {
guard let lastView = rightViews.last else {
owsFailDebug("Missing lastView")
return
}
@ -263,13 +205,8 @@ public class MediaAlbumView: UIStackView {
let itemCount = itemsToDisplay(forItems: items).count
switch itemCount {
case 0, 1, 4:
case 0, 1:
// X
//
// or
//
// XX
// XX
// Square
return CGSize(width: maxMessageWidth, height: maxMessageWidth)
@ -279,21 +216,13 @@ public class MediaAlbumView: UIStackView {
let imageSize = (maxMessageWidth - kSpacingPts) / 2
return CGSize(width: maxMessageWidth, height: imageSize)
case 3:
default:
// x
// X x
// Big on left, 2 small on right.
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
let bigImageSize = smallImageSize * 2 + kSpacingPts
return CGSize(width: maxMessageWidth, height: bigImageSize)
default:
// X X
// xxx
// 2 big on top, 3 small on bottom.
let bigImageSize = (maxMessageWidth - kSpacingPts) / 2
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts)
}
}

@ -60,6 +60,8 @@ public class MediaView: UIView {
backgroundColor = Colors.unimportant
clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = VisibleMessageCell.largeCornerRadius
createContents()
}

@ -20,15 +20,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
private lazy var profilePictureViewLeftConstraint = profilePictureView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.groupThreadHSpacing)
private lazy var profilePictureViewWidthConstraint = profilePictureView.set(.width, to: Values.verySmallProfilePictureSize)
private lazy var contentViewLeftConstraint1 = snContentView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing)
private lazy var contentViewLeftConstraint2 = snContentView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize)
private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
private lazy var contentViewRightConstraint1 = snContentView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
private lazy var contentViewRightConstraint2 = snContentView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize)
private lazy var bubbleViewLeftConstraint1 = bubbleView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing)
private lazy var bubbleViewLeftConstraint2 = bubbleView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize)
private lazy var bubbleViewTopConstraint = bubbleView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
private lazy var bubbleViewRightConstraint1 = bubbleView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
private lazy var bubbleViewRightConstraint2 = bubbleView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize)
private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: bubbleView)
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: bubbleView)
private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: snContentView)
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: snContentView)
private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0)
private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize)
@ -46,9 +45,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// MARK: - UI Components
private lazy var viewsToMoveForReply: [UIView] = [
bubbleView,
bubbleBackgroundView,
snContentView,
profilePictureView,
moderatorIconImageView,
replyButton,
timerView,
messageStatusImageView,
@ -87,7 +86,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return result
}()
private lazy var snContentView = UIView()
lazy var snContentView: UIStackView = {
let result = UIStackView(arrangedSubviews: [])
result.axis = .vertical
result.spacing = Values.verySmallSpacing
result.alignment = .leading
return result
}()
private lazy var reactionContainerView = ReactionContainerView()
internal lazy var messageStatusImageView: UIImageView = {
@ -147,6 +153,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return result
}()
static var leftGutterSize: CGFloat { groupThreadHSpacing + profilePictureSize + groupThreadHSpacing }
// MARK: Direction & Position
enum Direction { case incoming, outgoing }
@ -178,35 +186,31 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1)
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5)
// Bubble background view (used for the 'highlighted' animation)
addSubview(bubbleBackgroundView)
// Bubble view
addSubview(bubbleView)
bubbleViewLeftConstraint1.isActive = true
bubbleViewTopConstraint.isActive = true
bubbleViewRightConstraint1.isActive = true
bubbleView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1)
// Content view
addSubview(snContentView)
contentViewLeftConstraint1.isActive = true
contentViewTopConstraint.isActive = true
contentViewRightConstraint1.isActive = true
snContentView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1)
// Bubble background view
bubbleBackgroundView.addSubview(bubbleView)
bubbleBackgroundView.pin(to: bubbleView)
// Timer view
addSubview(timerView)
timerView.center(.vertical, in: bubbleView)
timerView.center(.vertical, in: snContentView)
timerViewOutgoingMessageConstraint.isActive = true
// Content view
bubbleView.addSubview(snContentView)
snContentView.pin(to: bubbleView)
// Reaction view
addSubview(reactionContainerView)
reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.verySmallSpacing)
reactionContainerView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing)
reactionContainerViewLeftConstraint.isActive = true
// Message status image view
addSubview(messageStatusImageView)
messageStatusImageViewTopConstraint.isActive = true
messageStatusImageView.pin(.right, to: .right, of: bubbleView, withInset: -1)
messageStatusImageView.pin(.right, to: .right, of: snContentView, withInset: -1)
messageStatusImageView.pin(.bottom, to: .bottom, of: self, withInset: -1)
messageStatusImageViewWidthConstraint.isActive = true
messageStatusImageViewHeightConstraint.isActive = true
@ -215,11 +219,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
addSubview(replyButton)
replyButton.addSubview(replyIconImageView)
replyIconImageView.center(in: replyButton)
replyButton.pin(.left, to: .right, of: bubbleView, withInset: Values.smallSpacing)
replyButton.center(.vertical, in: bubbleView)
replyButton.pin(.left, to: .right, of: snContentView, withInset: Values.smallSpacing)
replyButton.center(.vertical, in: snContentView)
// Remaining constraints
authorLabel.pin(.left, to: .left, of: bubbleView, withInset: VisibleMessageCell.authorLabelInset)
authorLabel.pin(.left, to: .left, of: snContentView, withInset: VisibleMessageCell.authorLabelInset)
}
override func setUpGestureRecognizers() {
@ -268,15 +272,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile)
// Bubble view
bubbleViewLeftConstraint1.isActive = (
contentViewLeftConstraint1.isActive = (
cellViewModel.variant == .standardIncoming ||
cellViewModel.variant == .standardIncomingDeleted
)
bubbleViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing)
bubbleViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing)
bubbleViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing)
bubbleViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing)
bubbleViewRightConstraint2.isActive = (
contentViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing)
contentViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing)
contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing)
contentViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing)
contentViewRightConstraint2.isActive = (
cellViewModel.variant == .standardIncoming ||
cellViewModel.variant == .standardIncomingDeleted
)
@ -395,12 +399,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
playbackInfo: ConversationViewModel.PlaybackInfo?,
lastSearchText: String?
) {
let direction: Direction = cellViewModel.variant == .standardOutgoing ? .outgoing : .incoming
let bodyLabelTextColor: UIColor = {
let direction: Direction = (cellViewModel.variant == .standardOutgoing ?
.outgoing :
.incoming
)
switch (direction, AppModeManager.shared.currentAppMode) {
case (.outgoing, .dark), (.incoming, .light): return .black
case (.outgoing, .light): return Colors.grey
@ -408,23 +408,33 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}
}()
snContentView.subviews.forEach { $0.removeFromSuperview() }
snContentView.alignment = direction == .incoming ? .leading : .trailing
for subview in snContentView.arrangedSubviews {
snContentView.removeArrangedSubview(subview)
subview.removeFromSuperview()
}
for subview in bubbleView.subviews {
subview.removeFromSuperview()
}
albumView = nil
bodyTappableLabel = nil
// Handle the deleted state first (it's much simpler than the others)
guard cellViewModel.variant != .standardIncomingDeleted else {
let deletedMessageView: DeletedMessageView = DeletedMessageView(textColor: bodyLabelTextColor)
snContentView.addSubview(deletedMessageView)
deletedMessageView.pin(to: snContentView)
bubbleView.addSubview(deletedMessageView)
deletedMessageView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
return
}
// If it's an incoming media message and the thread isn't trusted then show the placeholder view
if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor)
snContentView.addSubview(mediaPlaceholderView)
mediaPlaceholderView.pin(to: snContentView)
bubbleView.addSubview(mediaPlaceholderView)
mediaPlaceholderView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
return
}
@ -450,8 +460,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
bodyLabelTextColor: bodyLabelTextColor,
lastSearchText: lastSearchText
)
snContentView.addSubview(linkPreviewView)
linkPreviewView.pin(to: snContentView)
bubbleView.addSubview(linkPreviewView)
linkPreviewView.pin(to: bubbleView, withInset: 0)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.bodyTappableLabel = linkPreviewView.bodyTappableLabel
case .openGroupInvitation:
@ -461,9 +472,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
textColor: bodyLabelTextColor,
isOutgoing: (cellViewModel.variant == .standardOutgoing)
)
snContentView.addSubview(openGroupInvitationView)
openGroupInvitationView.pin(to: snContentView)
bubbleView.addSubview(openGroupInvitationView)
bubbleView.pin(to: openGroupInvitationView)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
}
else {
@ -506,15 +517,29 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
stackView.addArrangedSubview(bodyTappableLabel)
// Constraints
snContentView.addSubview(stackView)
stackView.pin(to: snContentView, withInset: inset)
bubbleView.addSubview(stackView)
stackView.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
case .mediaMessage:
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = Values.smallSpacing
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty {
let inset: CGFloat = 12
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self
)
self.bodyTappableLabel = bodyTappableLabel
bubbleView.addSubview(bodyTappableLabel)
bodyTappableLabel.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
// Album view
let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
@ -531,29 +556,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
albumView.set(.width, to: size.width)
albumView.set(.height, to: size.height)
albumView.loadMedia()
stackView.addArrangedSubview(albumView)
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty {
let inset: CGFloat = 12
let maxWidth: CGFloat = (size.width - (2 * inset))
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self
)
self.bodyTappableLabel = bodyTappableLabel
stackView.addArrangedSubview(UIView(wrapping: bodyTappableLabel, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset)))
}
snContentView.addArrangedSubview(albumView)
unloadContent = { albumView.unloadMedia() }
// Constraints
snContentView.addSubview(stackView)
stackView.pin(to: snContentView)
case .audio:
guard let attachment: Attachment = cellViewModel.attachments?.first(where: { $0.isAudio }) else {
return
@ -567,9 +573,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
playbackRate: (playbackInfo?.playbackRate ?? 1),
oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
)
snContentView.addSubview(voiceMessageView)
voiceMessageView.pin(to: snContentView)
bubbleView.addSubview(voiceMessageView)
voiceMessageView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.voiceMessageView = voiceMessageView
case .genericAttachment:
@ -577,7 +584,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
let inset: CGFloat = 12
let maxWidth = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
@ -586,7 +593,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// Document view
let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor)
stackView.addArrangedSubview(documentView)
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty { // delegate should always be set at this point
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
@ -601,9 +608,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
stackView.addArrangedSubview(bodyTappableLabel)
}
// Constraints
snContentView.addSubview(stackView)
stackView.pin(to: snContentView, withInset: inset)
bubbleView.addSubview(stackView)
stackView.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
}
@ -654,7 +661,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}
private func updateBubbleViewCorners() {
let cornersToRound: UIRectCorner = getCornersToRound()
let cornersToRound: UIRectCorner = .allCorners
bubbleBackgroundView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
bubbleBackgroundView.layer.maskedCorners = getCornerMask(from: cornersToRound)
@ -830,7 +837,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
delegate?.needsLayout(for: cellViewModel, expandingReactions: false)
}
}
else if bubbleView.frame.contains(location) {
else if snContentView.frame.contains(location) {
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer)
}
}
@ -899,10 +906,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// MARK: - Convenience
private func getCornersToRound() -> UIRectCorner {
return .allCorners
}
private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask {
guard !rectCorner.contains(.allCorners) else {
return [ .layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
@ -1015,7 +1018,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
cellViewModel.threadVariant == .openGroup ||
cellViewModel.threadVariant == .closedGroup
)
let leftGutterSize = (isGroupThread ? gutterSize : contactThreadHSpacing)
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
return (screen.width - leftGutterSize - gutterSize)

Loading…
Cancel
Save