mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
9.3 KiB
Swift
265 lines
9.3 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
|
|
public class MediaAlbumView: UIStackView {
|
|
private let items: [Attachment]
|
|
public let itemViews: [MediaView]
|
|
public var moreItemsView: MediaView?
|
|
public var numItems: Int { return items.count }
|
|
public var numVisibleItems: Int { return itemViews.count }
|
|
|
|
private static let kSpacingPts: CGFloat = 4
|
|
private static let kMaxItems = 3
|
|
|
|
@available(*, unavailable, message: "use other init() instead.")
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public required init(
|
|
mediaCache: NSCache<NSString, AnyObject>,
|
|
items: [Attachment],
|
|
isOutgoing: Bool,
|
|
maxMessageWidth: CGFloat
|
|
) {
|
|
let itemsToDisplay: [Attachment] = MediaAlbumView.itemsToDisplay(forItems: items)
|
|
|
|
self.items = items
|
|
self.itemViews = itemsToDisplay.enumerated()
|
|
.map { index, attachment -> MediaView in
|
|
MediaView(
|
|
mediaCache: mediaCache,
|
|
attachment: attachment,
|
|
isOutgoing: isOutgoing,
|
|
shouldSupressControls: (
|
|
// If there are extra items that aren't displayed and this is the
|
|
// last one that will be displayed then suppress any custom controls
|
|
// otherwise the '+' icon will be obscured
|
|
itemsToDisplay.count != items.count &&
|
|
(index == (itemsToDisplay.count - 1))
|
|
),
|
|
cornerRadius: VisibleMessageCell.largeCornerRadius
|
|
)
|
|
}
|
|
|
|
super.init(frame: .zero)
|
|
|
|
createContents(maxMessageWidth: maxMessageWidth)
|
|
}
|
|
|
|
private func createContents(maxMessageWidth: CGFloat) {
|
|
let backgroundView: UIView = UIView()
|
|
backgroundView.themeBackgroundColor = .backgroundPrimary
|
|
addSubview(backgroundView)
|
|
|
|
backgroundView.setContentHugging(to: .defaultLow)
|
|
backgroundView.setCompressionResistance(to: .defaultLow)
|
|
backgroundView.pin(to: backgroundView)
|
|
|
|
switch itemViews.count {
|
|
case 0: return Log.error("[MediaAlbumView] No item views.")
|
|
|
|
case 1:
|
|
// X
|
|
guard let itemView = itemViews.first else {
|
|
Log.error("[MediaAlbumView] Missing item view.")
|
|
return
|
|
}
|
|
addSubview(itemView)
|
|
itemView.pin(to: self)
|
|
|
|
case 2:
|
|
// X X
|
|
// side-by-side.
|
|
let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
|
|
autoSet(viewSize: imageSize, ofViews: itemViews)
|
|
for itemView in itemViews {
|
|
addArrangedSubview(itemView)
|
|
}
|
|
self.axis = .horizontal
|
|
self.distribution = .fillEqually
|
|
self.spacing = MediaAlbumView.kSpacingPts
|
|
|
|
default:
|
|
// x
|
|
// X x
|
|
// Big on left, 2 small on right.
|
|
let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3
|
|
let bigImageSize = smallImageSize * 2 + MediaAlbumView.kSpacingPts
|
|
|
|
guard let leftItemView = itemViews.first else {
|
|
Log.error("[MediaAlbumView] Missing view")
|
|
return
|
|
}
|
|
autoSet(viewSize: bigImageSize, ofViews: [leftItemView])
|
|
addArrangedSubview(leftItemView)
|
|
|
|
let rightViews = Array(itemViews[1..<3])
|
|
addArrangedSubview(
|
|
newRow(
|
|
rowViews: rightViews,
|
|
axis: .vertical,
|
|
viewSize: smallImageSize
|
|
)
|
|
)
|
|
self.axis = .horizontal
|
|
self.spacing = MediaAlbumView.kSpacingPts
|
|
|
|
if items.count > MediaAlbumView.kMaxItems {
|
|
guard let lastView = rightViews.last else {
|
|
Log.error("[MediaAlbumView] Missing lastView")
|
|
return
|
|
}
|
|
|
|
moreItemsView = lastView
|
|
|
|
let tintView = UIView()
|
|
tintView.themeBackgroundColor = .messageBubble_overlay
|
|
lastView.addSubview(tintView)
|
|
tintView.pin(to: self)
|
|
|
|
let moreCount = max(1, items.count - MediaAlbumView.kMaxItems)
|
|
let moreText = "andMore"
|
|
.put(key: "count", value: moreCount)
|
|
.localized()
|
|
let moreLabel: UILabel = UILabel()
|
|
moreLabel.font = .systemFont(ofSize: 24)
|
|
moreLabel.text = moreText
|
|
moreLabel.themeTextColor = .white
|
|
lastView.addSubview(moreLabel)
|
|
moreLabel.center(in: lastView)
|
|
}
|
|
}
|
|
|
|
for itemView in itemViews {
|
|
guard moreItemsView != itemView else {
|
|
// Don't display the caption indicator on
|
|
// the "more" item, if any.
|
|
continue
|
|
}
|
|
guard let index = itemViews.firstIndex(of: itemView) else {
|
|
Log.error("[MediaAlbumView] Couldn't determine index of item view.")
|
|
continue
|
|
}
|
|
let item = items[index]
|
|
guard let caption = item.caption else {
|
|
continue
|
|
}
|
|
guard caption.count > 0 else {
|
|
continue
|
|
}
|
|
guard let icon = UIImage(named: "media_album_caption") else {
|
|
Log.error("[MediaAlbumView] Couldn't load icon.")
|
|
continue
|
|
}
|
|
let iconView = UIImageView(image: icon)
|
|
itemView.addSubview(iconView)
|
|
itemView.layoutMargins = .zero
|
|
iconView.pin(.top, to: .top, of: itemView.layoutMarginsGuide, withInset: 6)
|
|
iconView.pin(.leading, to: .leading, of: itemView.layoutMarginsGuide, withInset: 6)
|
|
}
|
|
}
|
|
|
|
private func autoSet(
|
|
viewSize: CGFloat,
|
|
ofViews views: [MediaView]
|
|
) {
|
|
for itemView in views {
|
|
itemView.set(.width, to: viewSize)
|
|
itemView.set(.height, to: viewSize)
|
|
}
|
|
}
|
|
|
|
private func newRow(
|
|
rowViews: [MediaView],
|
|
axis: NSLayoutConstraint.Axis,
|
|
viewSize: CGFloat
|
|
) -> UIStackView {
|
|
autoSet(viewSize: viewSize, ofViews: rowViews)
|
|
return newRow(rowViews: rowViews, axis: axis)
|
|
}
|
|
|
|
private func newRow(
|
|
rowViews: [MediaView],
|
|
axis: NSLayoutConstraint.Axis
|
|
) -> UIStackView {
|
|
let stackView = UIStackView(arrangedSubviews: rowViews)
|
|
stackView.axis = axis
|
|
stackView.spacing = MediaAlbumView.kSpacingPts
|
|
return stackView
|
|
}
|
|
|
|
public func loadMedia() {
|
|
for itemView in itemViews {
|
|
itemView.loadMedia()
|
|
}
|
|
}
|
|
|
|
public func unloadMedia() {
|
|
for itemView in itemViews {
|
|
itemView.unloadMedia()
|
|
}
|
|
}
|
|
|
|
private class func itemsToDisplay(forItems items: [Attachment]) -> [Attachment] {
|
|
// TODO: Unless design changes, we want to display
|
|
// items which are still downloading and invalid
|
|
// items.
|
|
let validItems = items
|
|
guard validItems.count < kMaxItems else {
|
|
return Array(validItems[0..<kMaxItems])
|
|
}
|
|
return validItems
|
|
}
|
|
|
|
public class func layoutSize(
|
|
forMaxMessageWidth maxMessageWidth: CGFloat,
|
|
items: [Attachment]
|
|
) -> CGSize {
|
|
let itemCount = itemsToDisplay(forItems: items).count
|
|
|
|
switch itemCount {
|
|
case 0, 1:
|
|
// X
|
|
// Square
|
|
return CGSize(width: maxMessageWidth, height: maxMessageWidth)
|
|
|
|
case 2:
|
|
// X X
|
|
// side-by-side.
|
|
let imageSize = (maxMessageWidth - kSpacingPts) / 2
|
|
return CGSize(width: maxMessageWidth, height: imageSize)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
public func mediaView(forLocation location: CGPoint) -> MediaView? {
|
|
var bestMediaView: MediaView?
|
|
var bestDistance: CGFloat = 0
|
|
for itemView in itemViews {
|
|
let itemCenter = convert(itemView.center, from: itemView.superview)
|
|
let distance = location.distance(to: itemCenter)
|
|
if bestMediaView != nil && distance > bestDistance {
|
|
continue
|
|
}
|
|
bestMediaView = itemView
|
|
bestDistance = distance
|
|
}
|
|
return bestMediaView
|
|
}
|
|
|
|
public func isMoreItemsView(mediaView: MediaView) -> Bool {
|
|
return moreItemsView == mediaView
|
|
}
|
|
}
|