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.
633 lines
24 KiB
Swift
633 lines
24 KiB
Swift
//
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public protocol MediaTileViewControllerDelegate: class {
|
|
func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem)
|
|
}
|
|
|
|
public class MediaTileViewController: UICollectionViewController, MediaGalleryCellDelegate {
|
|
|
|
private weak var mediaGalleryDataSource: MediaGalleryDataSource?
|
|
|
|
private var galleryItems: [GalleryDate: [MediaGalleryItem]] {
|
|
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
|
|
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
|
|
return [:]
|
|
}
|
|
return mediaGalleryDataSource.sections
|
|
}
|
|
private var galleryDates: [GalleryDate] {
|
|
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
|
|
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
|
|
return []
|
|
}
|
|
return mediaGalleryDataSource.sectionDates
|
|
}
|
|
public var focusedItem: MediaGalleryItem?
|
|
|
|
private let uiDatabaseConnection: YapDatabaseConnection
|
|
|
|
public weak var delegate: MediaTileViewControllerDelegate?
|
|
|
|
deinit {
|
|
Logger.debug("\(logTag) deinit")
|
|
}
|
|
|
|
fileprivate let mediaTileViewLayout: MediaTileViewLayout
|
|
|
|
init(mediaGalleryDataSource: MediaGalleryDataSource, uiDatabaseConnection: YapDatabaseConnection) {
|
|
|
|
self.mediaGalleryDataSource = mediaGalleryDataSource
|
|
assert(uiDatabaseConnection.isInLongLivedReadTransaction())
|
|
self.uiDatabaseConnection = uiDatabaseConnection
|
|
|
|
// Layout Setup
|
|
|
|
let screenWidth = UIScreen.main.bounds.size.width
|
|
let kItemsPerRow = 4
|
|
let kInterItemSpacing: CGFloat = 2
|
|
|
|
let availableWidth = screenWidth - CGFloat(kItemsPerRow + 1) * kInterItemSpacing
|
|
let kItemWidth = floor(availableWidth / CGFloat(kItemsPerRow))
|
|
|
|
let layout: MediaTileViewLayout = MediaTileViewLayout()
|
|
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
layout.itemSize = CGSize(width: kItemWidth, height: kItemWidth)
|
|
layout.minimumInteritemSpacing = kInterItemSpacing
|
|
layout.minimumLineSpacing = kInterItemSpacing
|
|
layout.sectionHeadersPinToVisibleBounds = true
|
|
self.mediaTileViewLayout = layout
|
|
|
|
super.init(collectionViewLayout: layout)
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: View Lifecycle Overrides
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.title = MediaStrings.allMedia
|
|
|
|
guard let collectionView = self.collectionView else {
|
|
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
collectionView.backgroundColor = UIColor.white
|
|
|
|
collectionView.register(MediaGalleryCell.self, forCellWithReuseIdentifier: MediaGalleryCell.reuseIdentifier)
|
|
collectionView.register(MediaGallerySectionHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGallerySectionHeader.reuseIdentifier)
|
|
collectionView.register(MediaGalleryLoadingHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGalleryLoadingHeader.reuseIdentifier)
|
|
|
|
collectionView.delegate = self
|
|
|
|
// TODO iPhoneX
|
|
// feels a bit weird to have content smashed all the way to the bottom edge.
|
|
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)
|
|
|
|
self.view.layoutIfNeeded()
|
|
scrollToBottom(animated: false)
|
|
}
|
|
|
|
private func indexPath(galleryItem: MediaGalleryItem) -> IndexPath? {
|
|
guard let sectionIdx = galleryDates.index(of: galleryItem.galleryDate) else {
|
|
return nil
|
|
}
|
|
guard let rowIdx = galleryItems[galleryItem.galleryDate]!.index(of: galleryItem) else {
|
|
return nil
|
|
}
|
|
|
|
return IndexPath(row: rowIdx, section: sectionIdx + 1)
|
|
}
|
|
|
|
override public func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
guard let focusedItem = self.focusedItem else {
|
|
return
|
|
}
|
|
|
|
guard let indexPath = self.indexPath(galleryItem: focusedItem) else {
|
|
owsFail("\(logTag) unexpectedly unable to find indexPath for focusedItem: \(focusedItem)")
|
|
return
|
|
}
|
|
|
|
Logger.debug("\(logTag) scrolling to focused item at indexPath: \(indexPath)")
|
|
self.collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
|
|
}
|
|
|
|
// MARK: UIColletionViewDelegate
|
|
|
|
override public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.autoLoadMoreIfNecessary()
|
|
}
|
|
|
|
override public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
self.isUserScrolling = true
|
|
}
|
|
|
|
override public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
self.isUserScrolling = false
|
|
}
|
|
|
|
private var isUserScrolling: Bool = false {
|
|
didSet {
|
|
autoLoadMoreIfNecessary()
|
|
}
|
|
}
|
|
|
|
// MARK: UIColletionViewDataSource
|
|
|
|
override public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
return galleryItems.keys.count + 2
|
|
}
|
|
|
|
override public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection sectionIdx: Int) -> Int {
|
|
if sectionIdx == kLoadOlderSectionIdx {
|
|
// load older
|
|
return 0
|
|
}
|
|
|
|
if sectionIdx == loadNewerSectionIdx {
|
|
// load more recent
|
|
return 0
|
|
}
|
|
|
|
guard let sectionDate = self.galleryDates[safe: sectionIdx - 1] else {
|
|
owsFail("\(logTag) in \(#function) unknown section: \(sectionIdx)")
|
|
return 0
|
|
}
|
|
|
|
guard let section = self.galleryItems[sectionDate] else {
|
|
owsFail("\(logTag) in \(#function) no section for date: \(sectionDate)")
|
|
return 0
|
|
}
|
|
|
|
return section.count
|
|
}
|
|
|
|
override public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
|
|
|
let defaultView = UICollectionReusableView()
|
|
if (kind == UICollectionElementKindSectionHeader) {
|
|
switch indexPath.section {
|
|
case kLoadOlderSectionIdx:
|
|
guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: MediaGalleryLoadingHeader.reuseIdentifier, for: indexPath) as? MediaGalleryLoadingHeader else {
|
|
|
|
owsFail("\(logTag) in \(#function) unable to build section header for kLoadOlderSectionIdx")
|
|
return defaultView
|
|
}
|
|
let title = NSLocalizedString("GALLERY_TILES_LOADING_OLDER_LABEL", comment: "Label indicating loading is in progress")
|
|
sectionHeader.configure(title: title)
|
|
return sectionHeader
|
|
case loadNewerSectionIdx:
|
|
guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: MediaGalleryLoadingHeader.reuseIdentifier, for: indexPath) as? MediaGalleryLoadingHeader else {
|
|
|
|
owsFail("\(logTag) in \(#function) unable to build section header for kLoadOlderSectionIdx")
|
|
return defaultView
|
|
}
|
|
let title = NSLocalizedString("GALLERY_TILES_LOADING_MORE_RECENT_LABEL", comment: "Label indicating loading is in progress")
|
|
sectionHeader.configure(title: title)
|
|
return sectionHeader
|
|
default:
|
|
guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: MediaGallerySectionHeader.reuseIdentifier, for: indexPath) as? MediaGallerySectionHeader else {
|
|
owsFail("\(logTag) in \(#function) unable to build section header for indexPath: \(indexPath)")
|
|
return defaultView
|
|
}
|
|
guard let date = self.galleryDates[safe: indexPath.section - 1] else {
|
|
owsFail("\(logTag) in \(#function) unknown section for indexPath: \(indexPath)")
|
|
return defaultView
|
|
}
|
|
|
|
sectionHeader.configure(title: date.localizedString)
|
|
return sectionHeader
|
|
}
|
|
}
|
|
|
|
return defaultView
|
|
}
|
|
|
|
override public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
Logger.debug("\(logTag) in \(#function) indexPath: \(indexPath)")
|
|
|
|
let defaultCell = UICollectionViewCell()
|
|
|
|
switch indexPath.section {
|
|
case kLoadOlderSectionIdx:
|
|
owsFail("\(logTag) in \(#function) unexpected cell for kLoadOlderSectionIdx")
|
|
return defaultCell
|
|
case loadNewerSectionIdx:
|
|
owsFail("\(logTag) in \(#function) unexpected cell for loadNewerSectionIdx")
|
|
return defaultCell
|
|
default:
|
|
guard let sectionDate = self.galleryDates[safe: indexPath.section - 1] else {
|
|
owsFail("\(logTag) in \(#function) unknown section: \(indexPath.section)")
|
|
return defaultCell
|
|
}
|
|
|
|
guard let sectionItems = self.galleryItems[sectionDate] else {
|
|
owsFail("\(logTag) in \(#function) no section for date: \(sectionDate)")
|
|
return defaultCell
|
|
}
|
|
|
|
guard let galleryItem = sectionItems[safe: indexPath.row] else {
|
|
owsFail("\(logTag) in \(#function) no message for row: \(indexPath.row)")
|
|
return defaultCell
|
|
}
|
|
|
|
guard let cell = self.collectionView?.dequeueReusableCell(withReuseIdentifier: MediaGalleryCell.reuseIdentifier, for: indexPath) as? MediaGalleryCell else {
|
|
owsFail("\(logTag) in \(#function) unexpected cell for indexPath: \(indexPath)")
|
|
return defaultCell
|
|
}
|
|
|
|
cell.configure(item: galleryItem, delegate: self)
|
|
|
|
return cell
|
|
}
|
|
}
|
|
|
|
// MARK: UICollectionViewDelegateFlowLayout
|
|
|
|
public func collectionView(_ collectionView: UICollectionView,
|
|
layout collectionViewLayout: UICollectionViewLayout,
|
|
referenceSizeForHeaderInSection section: Int) -> CGSize {
|
|
|
|
let kHeaderHeight: CGFloat = 50
|
|
|
|
switch section {
|
|
case kLoadOlderSectionIdx:
|
|
// Show "loading older..." iff there is still older data to be fetched
|
|
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
|
|
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
|
|
return CGSize.zero
|
|
}
|
|
return mediaGalleryDataSource.hasFetchedOldest ? CGSize.zero : CGSize(width: 0, height: 100)
|
|
case loadNewerSectionIdx:
|
|
// Show "loading newer..." iff there is still more recent data to be fetched
|
|
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
|
|
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
|
|
return CGSize.zero
|
|
}
|
|
return mediaGalleryDataSource.hasFetchedMostRecent ? CGSize.zero : CGSize(width: 0, height: 100)
|
|
default:
|
|
return CGSize(width: 0, height: kHeaderHeight)
|
|
}
|
|
}
|
|
// MARK: MediaGalleryDelegate
|
|
|
|
fileprivate func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) {
|
|
Logger.debug("\(logTag) in \(#function)")
|
|
self.delegate?.mediaTileViewController(self, didTapView: cell.imageView, mediaGalleryItem: item)
|
|
}
|
|
|
|
// MARK: Lazy Loading
|
|
|
|
// This should be substantially larger than one screen size so we don't have to call it
|
|
// multiple times in a rapid succession, but not so large that loading get's really chopping
|
|
let kMediaTileViewLoadBatchSize: UInt = 40
|
|
var oldestLoadedItem: MediaGalleryItem? {
|
|
guard let oldestDate = galleryDates.first else {
|
|
return nil
|
|
}
|
|
|
|
return galleryItems[oldestDate]?.first
|
|
}
|
|
|
|
var mostRecentLoadedItem: MediaGalleryItem? {
|
|
guard let mostRecentDate = galleryDates.last else {
|
|
return nil
|
|
}
|
|
|
|
return galleryItems[mostRecentDate]?.last
|
|
}
|
|
|
|
var isFetchingMoreData: Bool = false
|
|
|
|
let kLoadOlderSectionIdx = 0
|
|
var loadNewerSectionIdx: Int {
|
|
return galleryDates.count + 1
|
|
}
|
|
|
|
public func autoLoadMoreIfNecessary() {
|
|
let kEdgeThreshold: CGFloat = 800
|
|
|
|
if (self.isUserScrolling) {
|
|
return
|
|
}
|
|
|
|
guard let collectionView = self.collectionView else {
|
|
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
|
|
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
let contentOffsetY = collectionView.contentOffset.y
|
|
let oldContentHeight = collectionView.contentSize.height
|
|
|
|
if contentOffsetY < kEdgeThreshold {
|
|
// Near the top, load older content
|
|
|
|
guard let oldestLoadedItem = self.oldestLoadedItem else {
|
|
Logger.debug("\(logTag) in \(#function) no oldest item")
|
|
return
|
|
}
|
|
|
|
guard !mediaGalleryDataSource.hasFetchedOldest else {
|
|
return
|
|
}
|
|
|
|
guard !isFetchingMoreData else {
|
|
Logger.debug("\(logTag) in \(#function) already fetching more data")
|
|
return
|
|
}
|
|
isFetchingMoreData = true
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
|
|
// mediaTileViewLayout will adjust content offset to compensate for the change in content height so that
|
|
// the same content is visible after the update. I considered doing something like setContentOffset in the
|
|
// batchUpdate completion block, but it caused a distinct flicker, which I was able to avoid with the
|
|
// `CollectionViewLayout.prepare` based approach.
|
|
mediaTileViewLayout.isInsertingCellsToTop = true
|
|
mediaTileViewLayout.contentSizeBeforeInsertingToTop = collectionView.contentSize
|
|
collectionView.performBatchUpdates({
|
|
mediaGalleryDataSource.ensureGalleryItemsLoaded(.before, item: oldestLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in
|
|
Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections) items: \(addedItems)")
|
|
|
|
collectionView.insertSections(addedSections)
|
|
collectionView.insertItems(at: addedItems)
|
|
}
|
|
}, completion: { finished in
|
|
Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)")
|
|
self.isFetchingMoreData = false
|
|
CATransaction.commit()
|
|
})
|
|
|
|
} else if oldContentHeight - contentOffsetY < kEdgeThreshold {
|
|
// Near the bottom, load newer content
|
|
|
|
guard let mostRecentLoadedItem = self.mostRecentLoadedItem else {
|
|
Logger.debug("\(logTag) in \(#function) no mostRecent item")
|
|
return
|
|
}
|
|
|
|
guard !mediaGalleryDataSource.hasFetchedMostRecent else {
|
|
return
|
|
}
|
|
|
|
guard !isFetchingMoreData else {
|
|
Logger.debug("\(logTag) in \(#function) already fetching more data")
|
|
return
|
|
}
|
|
isFetchingMoreData = true
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
UIView.performWithoutAnimation {
|
|
collectionView.performBatchUpdates({
|
|
mediaGalleryDataSource.ensureGalleryItemsLoaded(.after, item: mostRecentLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in
|
|
Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections), items: \(addedItems)")
|
|
collectionView.insertSections(addedSections)
|
|
collectionView.insertItems(at: addedItems)
|
|
}
|
|
}, completion: { finished in
|
|
Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)")
|
|
self.isFetchingMoreData = false
|
|
CATransaction.commit()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Util
|
|
|
|
private func scrollToBottom(animated isAnimated: Bool) {
|
|
guard let collectionView = self.collectionView else {
|
|
owsFail("\(self.logTag) in \(#function) collectionView was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
let yOffset: CGFloat = collectionView.contentSize.height - collectionView.bounds.size.height + collectionView.contentInset.bottom
|
|
let offset: CGPoint = CGPoint(x: 0, y: yOffset)
|
|
|
|
collectionView.setContentOffset(offset, animated: isAnimated)
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Helper Classes
|
|
|
|
// Accomodates remaining scrolled to the same "apparent" position when new content is insterted
|
|
// into the top of a collectionView. There are multiple ways to solve this problem, but this
|
|
// is the only one which avoided a perceptible flicker.
|
|
fileprivate class MediaTileViewLayout: UICollectionViewFlowLayout {
|
|
|
|
fileprivate var isInsertingCellsToTop: Bool = false
|
|
fileprivate var contentSizeBeforeInsertingToTop: CGSize?
|
|
|
|
override public func prepare() {
|
|
super.prepare()
|
|
|
|
if isInsertingCellsToTop {
|
|
if let collectionView = collectionView, let oldContentSize = contentSizeBeforeInsertingToTop {
|
|
let newContentSize = collectionViewContentSize
|
|
let contentOffsetY = collectionView.contentOffset.y + (newContentSize.height - oldContentSize.height)
|
|
let newOffset = CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY)
|
|
collectionView.setContentOffset(newOffset, animated: false)
|
|
}
|
|
contentSizeBeforeInsertingToTop = nil
|
|
isInsertingCellsToTop = false
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate class MediaGallerySectionHeader: UICollectionReusableView {
|
|
|
|
static let reuseIdentifier = "MediaGallerySectionHeader"
|
|
|
|
// HACK: scrollbar incorrectly appears *behind* section headers
|
|
// in collection view on iOS11 =(
|
|
private class AlwaysOnTopLayer: CALayer {
|
|
override var zPosition: CGFloat {
|
|
get { return 0 }
|
|
set {}
|
|
}
|
|
}
|
|
|
|
let label: UILabel
|
|
|
|
override class var layerClass: AnyClass {
|
|
get {
|
|
// HACK: scrollbar incorrectly appears *behind* section headers
|
|
// in collection view on iOS11 =(
|
|
if #available(iOS 11, *) {
|
|
return AlwaysOnTopLayer.self
|
|
} else {
|
|
return super.layerClass
|
|
}
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
label = UILabel()
|
|
|
|
let blurEffect = UIBlurEffect(style: .extraLight)
|
|
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
|
|
|
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(blurEffectView)
|
|
self.addSubview(label)
|
|
|
|
blurEffectView.autoPinEdgesToSuperviewEdges()
|
|
label.autoPinEdge(toSuperviewEdge: .trailing)
|
|
label.autoPinEdge(toSuperviewEdge: .leading, withInset: 10)
|
|
label.autoVCenterInSuperview()
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func configure(title: String) {
|
|
self.label.text = title
|
|
}
|
|
|
|
override public func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
|
|
self.label.text = nil
|
|
}
|
|
}
|
|
|
|
fileprivate protocol MediaGalleryCellDelegate: class {
|
|
func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem)
|
|
}
|
|
|
|
fileprivate class MediaGalleryLoadingHeader: UICollectionViewCell {
|
|
|
|
static let reuseIdentifier = "MediaGalleryLoadingHeader"
|
|
|
|
let label = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
// TODO add spinnner, start/stop animating on will/end display
|
|
addSubview(label)
|
|
|
|
label.autoCenterInSuperview()
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func configure(title: String) {
|
|
self.label.text = title
|
|
}
|
|
|
|
public override func prepareForReuse() {
|
|
self.label.text = nil
|
|
}
|
|
}
|
|
|
|
fileprivate class MediaGalleryCell: UICollectionViewCell {
|
|
|
|
static let reuseIdentifier = "MediaGalleryCell"
|
|
|
|
public let imageView: UIImageView
|
|
private var tapGesture: UITapGestureRecognizer!
|
|
|
|
private let badgeView: UIImageView
|
|
|
|
private var item: MediaGalleryItem?
|
|
public weak var delegate: MediaGalleryCellDelegate?
|
|
|
|
static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video")
|
|
static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif")
|
|
|
|
override init(frame: CGRect) {
|
|
self.imageView = UIImageView()
|
|
imageView.contentMode = .scaleAspectFill
|
|
|
|
self.badgeView = UIImageView()
|
|
badgeView.isHidden = true
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
|
self.addGestureRecognizer(tapGesture)
|
|
|
|
self.clipsToBounds = true
|
|
self.contentView.addSubview(imageView)
|
|
self.contentView.addSubview(badgeView)
|
|
|
|
imageView.autoPinEdgesToSuperviewEdges()
|
|
|
|
// Note assets were rendered to match exactly. We don't want to re-size with
|
|
// content mode lest they become less legible.
|
|
let kBadgeSize = CGSize(width: 18, height: 12)
|
|
badgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3)
|
|
badgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3)
|
|
badgeView.autoSetDimensions(to: kBadgeSize)
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func configure(item: MediaGalleryItem, delegate: MediaGalleryCellDelegate) {
|
|
self.item = item
|
|
self.imageView.image = item.thumbnailImage
|
|
if item.isVideo {
|
|
self.badgeView.isHidden = false
|
|
self.badgeView.image = MediaGalleryCell.videoBadgeImage
|
|
} else if item.isAnimated {
|
|
self.badgeView.isHidden = false
|
|
self.badgeView.image = MediaGalleryCell.animatedBadgeImage
|
|
} else {
|
|
assert(item.isImage)
|
|
self.badgeView.isHidden = true
|
|
}
|
|
|
|
self.delegate = delegate
|
|
}
|
|
|
|
override public func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
|
|
self.item = nil
|
|
self.imageView.image = nil
|
|
self.badgeView.isHidden = true
|
|
self.delegate = nil
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
func didTap(gestureRecognizer: UITapGestureRecognizer) {
|
|
guard let item = self.item else {
|
|
owsFail("\(logTag) item was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
self.delegate?.didTapCell(self, item: item)
|
|
}
|
|
}
|